Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-05 21:08:45 +00:00
parent e129eff883
commit 5875e92ecf
55 changed files with 1370 additions and 725 deletions

View File

@ -1 +1 @@
d92a2acbdcc9e20cac9e64692564556314f6e476
bfd3175bf92587f21d17e2107e1e7e2ee0fa69bc

View File

@ -1,10 +1,11 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlCard, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import statisticsLabels from '../constants';
export default {
components: {
GlCard,
GlLoadingIcon,
},
data() {
@ -26,20 +27,14 @@ export default {
</script>
<template>
<div class="gl-card">
<div class="gl-card-body">
<h4>{{ __('Statistics') }}</h4>
<gl-loading-icon v-if="isLoading" size="lg" class="my-3" />
<template v-else>
<p
v-for="statistic in getStatistics(statisticsLabels)"
:key="statistic.key"
class="js-stats"
>
{{ statistic.label }}
<span class="light float-right">{{ statistic.value }}</span>
</p>
</template>
</div>
</div>
<gl-card>
<h4>{{ __('Statistics') }}</h4>
<gl-loading-icon v-if="isLoading" size="lg" class="my-3" />
<template v-else>
<p v-for="statistic in getStatistics(statisticsLabels)" :key="statistic.key" class="js-stats">
{{ statistic.label }}
<span class="light float-right">{{ statistic.value }}</span>
</p>
</template>
</gl-card>
</template>

View File

@ -173,21 +173,23 @@ export default {
<template #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="groups.length">
<gl-dropdown-section-header>{{
$options.i18n.groupsSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group in groups"
:key="`${group.id}${group.name}`"
data-testid="group-dropdown-item"
:avatar-url="group.avatar_url"
is-check-item
:is-checked="isSelected(group)"
@click.native.capture.stop="onItemClick(group)"
>
{{ group.name }}
</gl-dropdown-item>
</template>
<div>
<template v-if="groups.length">
<gl-dropdown-section-header>{{
$options.i18n.groupsSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group in groups"
:key="`${group.id}${group.name}`"
data-testid="group-dropdown-item"
:avatar-url="group.avatar_url"
is-check-item
:is-checked="isSelected(group)"
@click.native.capture.stop="onItemClick(group)"
>
{{ group.name }}
</gl-dropdown-item>
</template>
</div>
</gl-dropdown>
</template>

View File

@ -0,0 +1,3 @@
import { initGroupRunnerShow } from '~/runner/group_runner_show';
initGroupRunnerShow();

View File

@ -66,8 +66,11 @@ export default {
</script>
<template>
<div>
<div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
<gl-icon ref="commitIcon" name="commit" class="mr-1" />
<div
v-if="commit"
class="gl-float-left gl-mr-5 gl-display-flex gl-align-items-center js-commit-info"
>
<gl-icon ref="commitIcon" name="commit" class="gl-mr-2" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.shortId }}
@ -76,8 +79,11 @@ export default {
</div>
</div>
<div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
<gl-icon name="tag" class="mr-1" />
<div
v-if="tagName"
class="gl-float-left gl-mr-5 gl-display-flex gl-align-items-center js-tag-info"
>
<gl-icon name="tag" class="gl-mr-2" />
<div v-gl-tooltip.bottom :title="__('Tag')">
<gl-link v-if="tagPath" :href="tagPath">
{{ tagName }}
@ -88,23 +94,23 @@ export default {
<div
v-if="releasedAt || author"
class="float-left d-flex align-items-center js-author-date-info"
class="gl-float-left gl-display-flex gl-align-items-center js-author-date-info"
>
<span class="text-secondary">{{ createdTime }}&nbsp;</span>
<span class="gl-text-secondary">{{ createdTime }}&nbsp;</span>
<template v-if="releasedAt">
<span
v-gl-tooltip.bottom
:title="tooltipTitle(releasedAt)"
class="text-secondary flex-shrink-0"
class="gl-text-secondary gl-flex-shrink-0"
>
{{ releasedAtTimeAgo }}&nbsp;
</span>
</template>
<div v-if="author" class="d-flex">
<span class="text-secondary">{{ __('by') }}&nbsp;</span>
<div v-if="author" class="gl-display-flex">
<span class="gl-text-secondary">{{ __('by') }}&nbsp;</span>
<user-avatar-link
class="gl-my-n1"
class="gl-my-n1 gl-display-flex"
:link-href="author.webUrl"
:img-src="author.avatarUrl"
:img-alt="userImageAltDescription"

View File

@ -1,16 +1,13 @@
<script>
import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { formatJobCount } from '../utils';
import RunnerDeleteButton from '../components/runner_delete_button.vue';
import RunnerEditButton from '../components/runner_edit_button.vue';
import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import RunnerJobs from '../components/runner_jobs.vue';
import { I18N_FETCH_ERROR } from '../constants';
import runnerQuery from '../graphql/show/runner.query.graphql';
import { captureException } from '../sentry_utils';
@ -19,17 +16,11 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
export default {
name: 'GroupRunnerShowApp',
components: {
GlBadge,
GlTab,
RunnerDeleteButton,
RunnerEditButton,
RunnerPauseButton,
RunnerHeader,
RunnerDetails,
RunnerJobs,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runnerId: {
@ -40,6 +31,11 @@ export default {
type: String,
required: true,
},
editGroupRunnerPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
@ -68,9 +64,6 @@ export default {
canDelete() {
return this.runner.userPermissions?.deleteRunner;
},
jobCount() {
return formatJobCount(this.runner?.jobCount);
},
},
errorCaptured(error) {
this.reportToSentry(error);
@ -90,25 +83,12 @@ export default {
<div>
<runner-header v-if="runner" :runner="runner">
<template #actions>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
<runner-edit-button v-if="canUpdate && editGroupRunnerPath" :href="editGroupRunnerPath" />
<runner-pause-button v-if="canUpdate" :runner="runner" />
<runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
</template>
</runner-header>
<runner-details :runner="runner">
<template #jobs-tab>
<gl-tab>
<template #title>
{{ s__('Runners|Jobs') }}
<gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
{{ jobCount }}
</gl-badge>
</template>
<runner-jobs v-if="runner" :runner="runner" />
</gl-tab>
</template>
</runner-details>
<runner-details :runner="runner" />
</div>
</template>

View File

@ -1,21 +1,18 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import GroupRunnerShowApp from './group_runner_show_app.vue';
Vue.use(VueApollo);
export const initAdminRunnerShow = (selector = '#js-group-runner-show') => {
showAlertFromLocalStorage();
export const initGroupRunnerShow = (selector = '#js-group-runner-show') => {
const el = document.querySelector(selector);
if (!el) {
return null;
}
const { runnerId, runnersPath } = el.dataset;
const { runnerId, runnersPath, editGroupRunnerPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@ -29,6 +26,7 @@ export const initAdminRunnerShow = (selector = '#js-group-runner-show') => {
props: {
runnerId,
runnersPath,
editGroupRunnerPath,
},
});
},

View File

@ -0,0 +1,210 @@
# frozen_string_literal: true
# A Redis backed session store for real-time collaboration. A session is defined
# by its documents and the users that join this session. An online user can have
# two states within the session: "active" and "away".
#
# By design, session must eventually be cleaned up. If this doesn't happen
# explicitly, all keys used within the session model must have an expiry
# timestamp set.
class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
# An awareness session expires automatically after 1 hour of no activity
SESSION_LIFETIME = 1.hour
private_constant :SESSION_LIFETIME
# Expire user awareness keys after some time of inactivity
USER_LIFETIME = 1.hour
private_constant :USER_LIFETIME
PRESENCE_LIFETIME = 10.minutes
private_constant :PRESENCE_LIFETIME
KEY_NAMESPACE = "gitlab:awareness"
private_constant :KEY_NAMESPACE
class << self
def for(value = nil)
# Creates a unique value for situations where we have no unique value to
# create a session with. This could be when creating a new issue, a new
# merge request, etc.
value = SecureRandom.uuid unless value.present?
# We use SHA-256 based session identifiers (similar to abbreviated git
# hashes). There is always a chance for Hash collisions (birthday
# problem), we therefore have to pick a good tradeoff between the amount
# of data stored and the probability of a collision.
#
# The approximate probability for a collision can be calculated:
#
# p ~= n^2 / 2m
# ~= (2^18)^2 / (2 * 16^15)
# ~= 2^36 / 2^61
#
# n is the number of awareness sessions and m the number of possibilities
# for each item. For a hex number, this is 16^c, where c is the number of
# characters. With 260k (~2^18) sessions, the probability for a collision
# is ~2^-25.
#
# The number of 15 is selected carefully. The integer representation fits
# nicely into a signed 64 bit integer and eventually allows Redis to
# optimize its memory usage. 16 chars would exceed the space for
# this datatype.
id = Digest::SHA256.hexdigest(value.to_s)[0, 15]
AwarenessSession.new(id)
end
end
def initialize(id)
@id = id
end
def join(user)
user_key = user_sessions_key(user.id)
with_redis do |redis|
redis.pipelined do |pipeline|
pipeline.sadd(user_key, id_i)
pipeline.expire(user_key, USER_LIFETIME.to_i)
pipeline.zadd(users_key, timestamp.to_f, user.id)
# We also mark for expiry when a session key is created (first user joins),
# because some users might never actively leave a session and the key could
# therefore become stale, w/o us noticing.
reset_session_expiry(pipeline)
end
end
nil
end
def leave(user)
user_key = user_sessions_key(user.id)
with_redis do |redis|
redis.pipelined do |pipeline|
pipeline.srem(user_key, id_i)
pipeline.zrem(users_key, user.id)
end
# cleanup orphan sessions and users
#
# this needs to be a second pipeline due to the delete operations being
# dependent on the result of the cardinality checks
user_sessions_count, session_users_count = redis.pipelined do |pipeline|
pipeline.scard(user_key)
pipeline.zcard(users_key)
end
redis.pipelined do |pipeline|
pipeline.del(user_key) unless user_sessions_count > 0
unless session_users_count > 0
pipeline.del(users_key)
@id = nil
end
end
end
nil
end
def present?(user, threshold: PRESENCE_LIFETIME)
with_redis do |redis|
user_timestamp = redis.zscore(users_key, user.id)
break false unless user_timestamp.present?
timestamp - user_timestamp < threshold
end
end
def away?(user, threshold: PRESENCE_LIFETIME)
!present?(user, threshold: threshold)
end
# Updates the last_activity timestamp for a user in this session
def touch!(user)
with_redis do |redis|
redis.pipelined do |pipeline|
pipeline.zadd(users_key, timestamp.to_f, user.id)
# extend the session lifetime due to user activity
reset_session_expiry(pipeline)
end
end
nil
end
def size
with_redis do |redis|
redis.zcard(users_key)
end
end
def users
User.where(id: user_ids)
end
def users_with_last_activity
user_ids, last_activities = user_ids_with_last_activity.transpose
users = User.where(id: user_ids)
users.zip(last_activities)
end
private
attr_reader :id
# converts session id from hex to integer representation
def id_i
Integer(id, 16) if id.present?
end
def users_key
"#{KEY_NAMESPACE}:session:#{id}:users"
end
def user_sessions_key(user_id)
"#{KEY_NAMESPACE}:user:#{user_id}:sessions"
end
def with_redis
Gitlab::Redis::SharedState.with do |redis|
yield redis if block_given?
end
end
def timestamp
Time.now.to_i
end
def user_ids
with_redis do |redis|
redis.zrange(users_key, 0, -1)
end
end
# Returns an array of tuples, where the first element in the tuple represents
# the user ID and the second part the last_activity timestamp.
def user_ids_with_last_activity
pairs = with_redis do |redis|
redis.zrange(users_key, 0, -1, with_scores: true)
end
# map data type of score (float) to Time
pairs.map do |user_id, score|
[user_id, Time.zone.at(score.to_i)]
end
end
# We want sessions to cleanup automatically after a certain period of
# inactivity. This sets the expiry timestamp for this session to
# [SESSION_LIFETIME].
def reset_session_expiry(redis)
redis.expire(users_key, SESSION_LIFETIME)
nil
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Awareness
extend ActiveSupport::Concern
KEY_NAMESPACE = "gitlab:awareness"
private_constant :KEY_NAMESPACE
def join(session)
session.join(self)
nil
end
def leave(session)
session.leave(self)
nil
end
def session_ids
with_redis do |redis|
redis
.smembers(user_sessions_key)
# converts session ids from (internal) integer to hex presentation
.map { |key| key.to_i.to_s(16) }
end
end
private
def user_sessions_key
"#{KEY_NAMESPACE}:user:#{id}:sessions"
end
def with_redis
Gitlab::Redis::SharedState.with do |redis|
yield redis if block_given?
end
end
end

View File

@ -34,7 +34,7 @@ module Integrations
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
Gitlab::HTTP.post(uri, body: params, use_read_total_timeout: true)
Gitlab::HTTP.post(uri, body: params)
end
end
end

View File

@ -155,7 +155,6 @@ module Integrations
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
params[:use_read_total_timeout] = true
params
end

View File

@ -94,7 +94,7 @@ module Integrations
result = false
begin
response = Gitlab::HTTP.head(self.project_url, verify: true, use_read_total_timeout: true)
response = Gitlab::HTTP.head(self.project_url, verify: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"

View File

@ -60,8 +60,7 @@ module Integrations
response = Gitlab::HTTP.try_get(
commit_status_path(sha, ref),
verify: enable_ssl_verification,
extra_log_info: { project_id: project_id },
use_read_total_timeout: true
extra_log_info: { project_id: project_id }
)
status =

View File

@ -29,7 +29,7 @@ module Integrations
end
def execute(_data)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
rescue StandardError
nil

View File

@ -49,7 +49,7 @@ module Integrations
# # => 'running'
#
def commit_status(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification, use_read_total_timeout: true)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error

View File

@ -25,7 +25,7 @@ module Integrations
# support for `test` method
def execute(_data)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
rescue StandardError
nil

View File

@ -156,7 +156,7 @@ module Integrations
end
def get_path(path)
Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true)
Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
end
def post_to_build_queue(data, branch)
@ -167,8 +167,7 @@ module Integrations
'</build>',
headers: { 'Content-type' => 'application/xml' },
verify: enable_ssl_verification,
basic_auth: basic_auth,
use_read_total_timeout: true
basic_auth: basic_auth
)
end

View File

@ -46,8 +46,7 @@ module Integrations
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
markdown: true,
use_read_total_timeout: true
markdown: true
}.to_json)
response if response.success?

View File

@ -44,7 +44,7 @@ module Integrations
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json, use_read_total_timeout: true)
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
response if response.success?
end

View File

@ -9,6 +9,7 @@ class User < ApplicationRecord
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
include Awareness
include Referable
include Sortable
include CaseSensitivity

View File

@ -48,7 +48,6 @@ class WebHookService
@force = force
@request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout,
use_read_total_timeout: true,
allow_local_requests: hook.allow_local_requests?
}
end

View File

@ -1,6 +1,10 @@
- add_to_breadcrumbs _('Runners'), group_runners_path(@group)
- if Feature.enabled?(:group_runner_view_ui)
#js-group-runner-show{ data: {runner_id: @runner.id, runners_path: group_runners_path(@group)} }
- title = "##{@runner.id} (#{@runner.short_sha})"
- breadcrumb_title title
- page_title title
#js-group-runner-show{ data: {runner_id: @runner.id, runners_path: group_runners_path(@group), edit_group_runner_path: edit_group_runner_path(@group, @runner)} }
- else
= render 'shared/runners/runner_details', runner: @runner

View File

@ -1293,6 +1293,9 @@ production: &base
prometheus:
# enabled: true
# server_address: 'localhost:9090'
snowplow_micro:
enabled: true
address: '127.0.0.1:9091'
## Consul settings
consul:

View File

@ -519,7 +519,7 @@ To solve this:
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<first_failed_geo_sync_ID>"
```
1. Enter the [Rails console](../../troubleshooting/navigating_gitlab_via_rails_console.md) and run:
1. Enter the [Rails console](../../operations/rails_console.md) and run:
```ruby
failed_geo_syncs = Geo::ProjectRegistry.failed.pluck(:id)
@ -805,7 +805,7 @@ You can work around this by marking the objects as synced and succeeded verifica
be aware that can also mark objects that may be
[missing from the primary](#missing-files-on-the-geo-primary-site).
To do that, enter the [Rails console](../../troubleshooting/navigating_gitlab_via_rails_console.md)
To do that, enter the [Rails console](../../operations/rails_console.md)
and run:
```ruby

View File

@ -211,7 +211,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Log system](logs.md): Where to look for logs.
- [Sidekiq Troubleshooting](troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs.
- [Troubleshooting Elasticsearch](troubleshooting/elasticsearch.md)
- [Navigating GitLab via Rails console](troubleshooting/navigating_gitlab_via_rails_console.md)
- [Navigating GitLab via Rails console](operations/rails_console.md)
- [GitLab application limits](instance_limits.md)
- [Responding to security incidents](../security/responding_to_security_incidents.md)

View File

@ -6,8 +6,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Rails console **(FREE SELF)**
At the heart of GitLab is a web application [built using the Ruby on Rails
framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/).
The [Rails console](https://guides.rubyonrails.org/command_line.html#rails-console).
provides a way to interact with your GitLab instance from the command line.
provides a way to interact with your GitLab instance from the command line, and also grants access to the amazing tools built right into Rails.
WARNING:
The Rails console interacts directly with GitLab. In many cases,
@ -17,7 +19,9 @@ with no consequences, you are strongly advised to do so in a test environment.
The Rails console is for GitLab system administrators who are troubleshooting
a problem or need to retrieve some data that can only be done through direct
access of the GitLab application.
access of the GitLab application. Basic knowledge of Ruby is needed (try [this
30-minute tutorial](https://try.ruby-lang.org/) for a quick introduction).
Rails experience is useful but not required.
## Starting a Rails console session
@ -168,3 +172,435 @@ sudo chown -R git:git /scripts
sudo chmod 700 /scripts
sudo gitlab-rails runner /scripts/helloworld.rb
```
## Active Record objects
### Looking up database-persisted objects
Under the hood, Rails uses [Active Record](https://guides.rubyonrails.org/active_record_basics.html),
an object-relational mapping system, to read, write, and map application objects
to the PostgreSQL database. These mappings are handled by Active Record models,
which are Ruby classes defined in a Rails app. For GitLab, the model classes
can be found at `/opt/gitlab/embedded/service/gitlab-rails/app/models`.
Let's enable debug logging for Active Record so we can see the underlying
database queries made:
```ruby
ActiveRecord::Base.logger = Logger.new($stdout)
```
Now, let's try retrieving a user from the database:
```ruby
user = User.find(1)
```
Which would return:
```ruby
D, [2020-03-05T16:46:25.571238 #910] DEBUG -- : User Load (1.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> #<User id:1 @root>
```
We can see that we've queried the `users` table in the database for a row whose
`id` column has the value `1`, and Active Record has translated that database
record into a Ruby object that we can interact with. Try some of the following:
- `user.username`
- `user.created_at`
- `user.admin`
By convention, column names are directly translated into Ruby object attributes,
so you should be able to do `user.<column_name>` to view the attribute's value.
Also by convention, Active Record class names (singular and in camel case) map
directly onto table names (plural and in snake case) and vice versa. For example,
the `users` table maps to the `User` class, while the `application_settings`
table maps to the `ApplicationSetting` class.
You can find a list of tables and column names in the Rails database schema,
available at `/opt/gitlab/embedded/service/gitlab-rails/db/schema.rb`.
You can also look up an object from the database by attribute name:
```ruby
user = User.find_by(username: 'root')
```
Which would return:
```ruby
D, [2020-03-05T17:03:24.696493 #910] DEBUG -- : User Load (2.1ms) SELECT "users".* FROM "users" WHERE "users"."username" = 'root' LIMIT 1
=> #<User id:1 @root>
```
Give the following a try:
- `User.find_by(email: 'admin@example.com')`
- `User.where.not(admin: true)`
- `User.where('created_at < ?', 7.days.ago)`
Did you notice that the last two commands returned an `ActiveRecord::Relation`
object that appeared to contain multiple `User` objects?
Up to now, we've been using `.find` or `.find_by`, which are designed to return
only a single object (notice the `LIMIT 1` in the generated SQL query?).
`.where` is used when it is desirable to get a collection of objects.
Let's get a collection of non-administrator users and see what we can do with it:
```ruby
users = User.where.not(admin: true)
```
Which would return:
```ruby
D, [2020-03-05T17:11:16.845387 #910] DEBUG -- : User Load (2.8ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE LIMIT 11
=> #<ActiveRecord::Relation [#<User id:3 @support-bot>, #<User id:7 @alert-bot>, #<User id:5 @carrie>, #<User id:4 @bernice>, #<User id:2 @anne>]>
```
Now, try the following:
- `users.count`
- `users.order(created_at: :desc)`
- `users.where(username: 'support-bot')`
In the last command, we see that we can chain `.where` statements to generate
more complex queries. Notice also that while the collection returned contains
only a single object, we cannot directly interact with it:
```ruby
users.where(username: 'support-bot').username
```
Which would return:
```ruby
Traceback (most recent call last):
1: from (irb):37
D, [2020-03-05T17:18:25.637607 #910] DEBUG -- : User Load (1.6ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' LIMIT 11
NoMethodError (undefined method `username' for #<ActiveRecord::Relation [#<User id:3 @support-bot>]>)
Did you mean? by_username
```
Let's retrieve the single object from the collection by using the `.first`
method to get the first item in the collection:
```ruby
users.where(username: 'support-bot').first.username
```
We now get the result we wanted:
```ruby
D, [2020-03-05T17:18:30.406047 #910] DEBUG -- : User Load (2.6ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' ORDER BY "users"."id" ASC LIMIT 1
=> "support-bot"
```
For more on different ways to retrieve data from the database using Active
Record, please see the [Active Record Query Interface documentation](https://guides.rubyonrails.org/active_record_querying.html).
### Modifying Active Record objects
In the previous section, we learned about retrieving database records using
Active Record. Now, let's learn how to write changes to the database.
First, let's retrieve the `root` user:
```ruby
user = User.find_by(username: 'root')
```
Next, let's try updating the user's password:
```ruby
user.password = 'password'
user.save
```
Which would return:
```ruby
Enqueued ActionMailer::MailDeliveryJob (Job ID: 05915c4e-c849-4e14-80bb-696d5ae22065) to Sidekiq(mailers) with arguments: "DeviseMailer", "password_change", "deliver_now", #<GlobalID:0x00007f42d8ccebe8 @uri=#<URI::GID gid://gitlab/User/1>>
=> true
```
Here, we see that the `.save` command returned `true`, indicating that the
password change was successfully saved to the database.
We also see that the save operation triggered some other action -- in this case
a background job to deliver an email notification. This is an example of an
[Active Record callback](https://guides.rubyonrails.org/active_record_callbacks.html)
-- code which is designated to run in response to events in the Active Record
object life cycle. This is also why using the Rails console is preferred when
direct changes to data is necessary as changes made via direct database queries
do not trigger these callbacks.
It's also possible to update attributes in a single line:
```ruby
user.update(password: 'password')
```
Or update multiple attributes at once:
```ruby
user.update(password: 'password', email: 'hunter2@example.com')
```
Now, let's try something different:
```ruby
# Retrieve the object again so we get its latest state
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save
```
This returns `false`, indicating that the changes we made were not saved to the
database. You can probably guess why, but let's find out for sure:
```ruby
user.save!
```
This should return:
```ruby
Traceback (most recent call last):
1: from (irb):64
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password)
```
Aha! We've tripped an [Active Record Validation](https://guides.rubyonrails.org/active_record_validations.html).
Validations are business logic put in place at the application-level to prevent
unwanted data from being saved to the database and in most cases come with
helpful messages letting you know how to fix the problem inputs.
We can also add the bang (Ruby speak for `!`) to `.update`:
```ruby
user.update!(password: 'password', password_confirmation: 'hunter2')
```
In Ruby, method names ending with `!` are commonly known as "bang methods". By
convention, the bang indicates that the method directly modifies the object it
is acting on, as opposed to returning the transformed result and leaving the
underlying object untouched. For Active Record methods that write to the
database, bang methods also serve an additional function: they raise an
explicit exception whenever an error occurs, instead of just returning `false`.
We can also skip validations entirely:
```ruby
# Retrieve the object again so we get its latest state
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save!(validate: false)
```
This is not recommended, as validations are usually put in place to ensure the
integrity and consistency of user-provided data.
A validation error prevents the entire object from being saved to
the database. You can see a little of this in the section below. If you're getting
a mysterious red banner in the GitLab UI when submitting a form, this can often
be the fastest way to get to the root of the problem.
### Interacting with Active Record objects
At the end of the day, Active Record objects are just normal Ruby objects. As
such, we can define methods on them which perform arbitrary actions.
For example, GitLab developers have added some methods which help with
two-factor authentication:
```ruby
def disable_two_factor!
transaction do
update(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
end
```
(See: `/opt/gitlab/embedded/service/gitlab-rails/app/models/user.rb`)
We can then use these methods on any user object:
```ruby
user = User.find_by(username: 'root')
user.two_factor_enabled?
user.disable_two_factor!
```
Some methods are defined by gems, or Ruby software packages, which GitLab uses.
For example, the [StateMachines](https://github.com/state-machines/state_machines-activerecord)
gem which GitLab uses to manage user state:
```ruby
state_machine :state, initial: :active do
event :block do
...
event :activate do
...
end
```
Give it a try:
```ruby
user = User.find_by(username: 'root')
user.state
user.block
user.state
user.activate
user.state
```
Earlier, we mentioned that a validation error prevents the entire object
from being saved to the database. Let's see how this can have unexpected
interactions:
```ruby
user.password = 'password'
user.password_confirmation = 'hunter2'
user.block
```
We get `false` returned! Let's find out what happened by adding a bang as we did
earlier:
```ruby
user.block!
```
Which would return:
```ruby
Traceback (most recent call last):
1: from (irb):87
StateMachines::InvalidTransition (Cannot transition state via :block from :active (Reason(s): Password confirmation doesn't match Password))
```
We see that a validation error from what feels like a completely separate
attribute comes back to haunt us when we try to update the user in any way.
In practical terms, we sometimes see this happen with GitLab administration settings --
validations are sometimes added or changed in a GitLab update, resulting in
previously saved settings now failing validation. Because you can only update
a subset of settings at once through the UI, in this case the only way to get
back to a good state is direct manipulation via Rails console.
### Commonly used Active Record models and how to look up objects
**Get a user by primary email address or username:**
```ruby
User.find_by(email: 'admin@example.com')
User.find_by(username: 'root')
```
**Get a user by primary OR secondary email address:**
```ruby
User.find_by_any_email('user@example.com')
```
The `find_by_any_email` method is a custom method added by GitLab developers rather
than a Rails-provided default method.
**Get a collection of administrator users:**
```ruby
User.admins
```
`admins` is a [scope convenience method](https://guides.rubyonrails.org/active_record_querying.html#scopes)
which does `where(admin: true)` under the hood.
**Get a project by its path:**
```ruby
Project.find_by_full_path('group/subgroup/project')
```
`find_by_full_path` is a custom method added by GitLab developers rather
than a Rails-provided default method.
**Get a project's issue or merge request by its numeric ID:**
```ruby
project = Project.find_by_full_path('group/subgroup/project')
project.issues.find_by(iid: 42)
project.merge_requests.find_by(iid: 42)
```
`iid` means "internal ID" and is how we keep issue and merge request IDs
scoped to each GitLab project.
**Get a group by its path:**
```ruby
Group.find_by_full_path('group/subgroup')
```
**Get a group's related groups:**
```ruby
group = Group.find_by_full_path('group/subgroup')
# Get a group's parent group
group.parent
# Get a group's child groups
group.children
```
**Get a group's projects:**
```ruby
group = Group.find_by_full_path('group/subgroup')
# Get group's immediate child projects
group.projects
# Get group's child projects, including those in subgroups
group.all_projects
```
**Get CI pipeline or builds:**
```ruby
Ci::Pipeline.find(4151)
Ci::Build.find(66124)
```
The pipeline and job ID numbers increment globally across your GitLab
instance, so there's no requirement to use an internal ID attribute to look them up,
unlike with issues or merge requests.
**Get the current application settings object:**
```ruby
ApplicationSetting.current
```

View File

@ -887,7 +887,7 @@ administrators can clean up image tags
and [run garbage collection](#container-registry-garbage-collection).
To remove image tags by running the cleanup policy, run the following commands in the
[GitLab Rails console](../troubleshooting/navigating_gitlab_via_rails_console.md):
[GitLab Rails console](../operations/rails_console.md):
```ruby
# Numeric ID of the project whose container registry should be cleaned up
@ -1738,7 +1738,7 @@ In this case, follow these steps:
1. Try the removal again.
If you still can't remove the repository using the common methods, you can use the
[GitLab Rails console](../troubleshooting/navigating_gitlab_via_rails_console.md)
[GitLab Rails console](../operations/rails_console.md)
to remove the project by force:
```ruby

View File

@ -18,7 +18,6 @@ Your type of GitLab installation determines how
See also:
- [GitLab Rails Console Cheat Sheet](gitlab_rails_cheat_sheet.md).
- [Navigating GitLab via Rails console](navigating_gitlab_via_rails_console.md).
### Enabling Active Record logging

View File

@ -10,7 +10,7 @@ This is the GitLab Support Team's collection of information regarding the GitLab
console, for use while troubleshooting. It is listed here for transparency,
and it may be useful for users with experience with these tools. If you are currently
having an issue with GitLab, it is highly recommended that you first check
our guide on [navigating our Rails console](navigating_gitlab_via_rails_console.md),
our guide on [our Rails console](../operations/rails_console.md),
and your [support options](https://about.gitlab.com/support/), before attempting to use
this information.
@ -517,7 +517,7 @@ If this all runs successfully, you see an output like the following before being
The exported project is located within a `.tar.gz` file in `/var/opt/gitlab/gitlab-rails/uploads/-/system/import_export_upload/export_file/`.
If this fails, [enable verbose logging](navigating_gitlab_via_rails_console.md#looking-up-database-persisted-objects),
If this fails, [enable verbose logging](../operations/rails_console.md#looking-up-database-persisted-objects),
repeat the above procedure after,
and report the output to
[GitLab Support](https://about.gitlab.com/support/).
@ -1114,7 +1114,7 @@ License.select(&TYPE).each(&:destroy!)
As a GitLab administrator, you may need to reduce disk space consumption.
A common culprit is Docker Registry images that are no longer in use. To find
the storage broken down by each project, run the following in the
[GitLab Rails console](../troubleshooting/navigating_gitlab_via_rails_console.md):
[GitLab Rails console](../operations/rails_console.md):
```ruby
projects_and_size = [["project_id", "creator_id", "registry_size_bytes", "project path"]]

View File

@ -20,7 +20,6 @@ installation.
- [Kubernetes cheat sheet](kubernetes_cheat_sheet.md)
- [Linux cheat sheet](linux_cheat_sheet.md)
- [Parsing GitLab logs with `jq`](log_parsing.md)
- [Navigating GitLab via Rails console](navigating_gitlab_via_rails_console.md)
- [Diagnostics tools](diagnostics_tools.md)
- [Debugging tips](debug.md)
- [Tracing requests with correlation ID](tracing_correlation_id.md)

View File

@ -1,465 +1,11 @@
---
stage: Systems
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
redirect_to: '../operations/rails_console.md'
remove_date: '2022-10-05'
---
# Navigating GitLab via Rails console **(FREE SELF)**
This document was moved to [another location](../operations/rails_console.md).
At the heart of GitLab is a web application [built using the Ruby on Rails
framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/).
Thanks to this, we also get access to the amazing tools built right into Rails.
This guide introduces the [Rails console](../operations/rails_console.md#starting-a-rails-console-session)
and the basics of interacting with your GitLab instance from the command line.
WARNING:
The Rails console interacts directly with your GitLab instance. In many cases,
there are no handrails to prevent you from permanently modifying, corrupting
or destroying production data. If you would like to explore the Rails console
with no consequences, you are strongly advised to do so in a test environment.
This guide is targeted at GitLab system administrators who are troubleshooting
a problem or must retrieve some data that can only be done through direct
access of the GitLab application. Basic knowledge of Ruby is needed (try [this
30-minute tutorial](https://try.ruby-lang.org/) for a quick introduction).
Rails experience is helpful to have but not a must.
## Starting a Rails console session
Your type of GitLab installation determines how
[to start a rails console](../operations/rails_console.md).
The following code examples take place inside the Rails console and also
assume an Omnibus GitLab installation.
## Active Record objects
### Looking up database-persisted objects
Under the hood, Rails uses [Active Record](https://guides.rubyonrails.org/active_record_basics.html),
an object-relational mapping system, to read, write, and map application objects
to the PostgreSQL database. These mappings are handled by Active Record models,
which are Ruby classes defined in a Rails app. For GitLab, the model classes
can be found at `/opt/gitlab/embedded/service/gitlab-rails/app/models`.
Let's enable debug logging for Active Record so we can see the underlying
database queries made:
```ruby
ActiveRecord::Base.logger = Logger.new($stdout)
```
Now, let's try retrieving a user from the database:
```ruby
user = User.find(1)
```
Which would return:
```ruby
D, [2020-03-05T16:46:25.571238 #910] DEBUG -- : User Load (1.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> #<User id:1 @root>
```
We can see that we've queried the `users` table in the database for a row whose
`id` column has the value `1`, and Active Record has translated that database
record into a Ruby object that we can interact with. Try some of the following:
- `user.username`
- `user.created_at`
- `user.admin`
By convention, column names are directly translated into Ruby object attributes,
so you should be able to do `user.<column_name>` to view the attribute's value.
Also by convention, Active Record class names (singular and in camel case) map
directly onto table names (plural and in snake case) and vice versa. For example,
the `users` table maps to the `User` class, while the `application_settings`
table maps to the `ApplicationSetting` class.
You can find a list of tables and column names in the Rails database schema,
available at `/opt/gitlab/embedded/service/gitlab-rails/db/schema.rb`.
You can also look up an object from the database by attribute name:
```ruby
user = User.find_by(username: 'root')
```
Which would return:
```ruby
D, [2020-03-05T17:03:24.696493 #910] DEBUG -- : User Load (2.1ms) SELECT "users".* FROM "users" WHERE "users"."username" = 'root' LIMIT 1
=> #<User id:1 @root>
```
Give the following a try:
- `User.find_by(email: 'admin@example.com')`
- `User.where.not(admin: true)`
- `User.where('created_at < ?', 7.days.ago)`
Did you notice that the last two commands returned an `ActiveRecord::Relation`
object that appeared to contain multiple `User` objects?
Up to now, we've been using `.find` or `.find_by`, which are designed to return
only a single object (notice the `LIMIT 1` in the generated SQL query?).
`.where` is used when it is desirable to get a collection of objects.
Let's get a collection of non-administrator users and see what we can do with it:
```ruby
users = User.where.not(admin: true)
```
Which would return:
```ruby
D, [2020-03-05T17:11:16.845387 #910] DEBUG -- : User Load (2.8ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE LIMIT 11
=> #<ActiveRecord::Relation [#<User id:3 @support-bot>, #<User id:7 @alert-bot>, #<User id:5 @carrie>, #<User id:4 @bernice>, #<User id:2 @anne>]>
```
Now, try the following:
- `users.count`
- `users.order(created_at: :desc)`
- `users.where(username: 'support-bot')`
In the last command, we see that we can chain `.where` statements to generate
more complex queries. Notice also that while the collection returned contains
only a single object, we cannot directly interact with it:
```ruby
users.where(username: 'support-bot').username
```
Which would return:
```ruby
Traceback (most recent call last):
1: from (irb):37
D, [2020-03-05T17:18:25.637607 #910] DEBUG -- : User Load (1.6ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' LIMIT 11
NoMethodError (undefined method `username' for #<ActiveRecord::Relation [#<User id:3 @support-bot>]>)
Did you mean? by_username
```
Let's retrieve the single object from the collection by using the `.first`
method to get the first item in the collection:
```ruby
users.where(username: 'support-bot').first.username
```
We now get the result we wanted:
```ruby
D, [2020-03-05T17:18:30.406047 #910] DEBUG -- : User Load (2.6ms) SELECT "users".* FROM "users" WHERE "users"."admin" != TRUE AND "users"."username" = 'support-bot' ORDER BY "users"."id" ASC LIMIT 1
=> "support-bot"
```
For more on different ways to retrieve data from the database using Active
Record, please see the [Active Record Query Interface documentation](https://guides.rubyonrails.org/active_record_querying.html).
### Modifying Active Record objects
In the previous section, we learned about retrieving database records using
Active Record. Now, let's learn how to write changes to the database.
First, let's retrieve the `root` user:
```ruby
user = User.find_by(username: 'root')
```
Next, let's try updating the user's password:
```ruby
user.password = 'password'
user.save
```
Which would return:
```ruby
Enqueued ActionMailer::MailDeliveryJob (Job ID: 05915c4e-c849-4e14-80bb-696d5ae22065) to Sidekiq(mailers) with arguments: "DeviseMailer", "password_change", "deliver_now", #<GlobalID:0x00007f42d8ccebe8 @uri=#<URI::GID gid://gitlab/User/1>>
=> true
```
Here, we see that the `.save` command returned `true`, indicating that the
password change was successfully saved to the database.
We also see that the save operation triggered some other action -- in this case
a background job to deliver an email notification. This is an example of an
[Active Record callback](https://guides.rubyonrails.org/active_record_callbacks.html)
-- code which is designated to run in response to events in the Active Record
object life cycle. This is also why using the Rails console is preferred when
direct changes to data is necessary as changes made via direct database queries
do not trigger these callbacks.
It's also possible to update attributes in a single line:
```ruby
user.update(password: 'password')
```
Or update multiple attributes at once:
```ruby
user.update(password: 'password', email: 'hunter2@example.com')
```
Now, let's try something different:
```ruby
# Retrieve the object again so we get its latest state
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save
```
This returns `false`, indicating that the changes we made were not saved to the
database. You can probably guess why, but let's find out for sure:
```ruby
user.save!
```
This should return:
```ruby
Traceback (most recent call last):
1: from (irb):64
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password)
```
Aha! We've tripped an [Active Record Validation](https://guides.rubyonrails.org/active_record_validations.html).
Validations are business logic put in place at the application-level to prevent
unwanted data from being saved to the database and in most cases come with
helpful messages letting you know how to fix the problem inputs.
We can also add the bang (Ruby speak for `!`) to `.update`:
```ruby
user.update!(password: 'password', password_confirmation: 'hunter2')
```
In Ruby, method names ending with `!` are commonly known as "bang methods". By
convention, the bang indicates that the method directly modifies the object it
is acting on, as opposed to returning the transformed result and leaving the
underlying object untouched. For Active Record methods that write to the
database, bang methods also serve an additional function: they raise an
explicit exception whenever an error occurs, instead of just returning `false`.
We can also skip validations entirely:
```ruby
# Retrieve the object again so we get its latest state
user = User.find_by(username: 'root')
user.password = 'password'
user.password_confirmation = 'hunter2'
user.save!(validate: false)
```
This is not recommended, as validations are usually put in place to ensure the
integrity and consistency of user-provided data.
A validation error prevents the entire object from being saved to
the database. You can see a little of this in the section below. If you're getting
a mysterious red banner in the GitLab UI when submitting a form, this can often
be the fastest way to get to the root of the problem.
### Interacting with Active Record objects
At the end of the day, Active Record objects are just normal Ruby objects. As
such, we can define methods on them which perform arbitrary actions.
For example, GitLab developers have added some methods which help with
two-factor authentication:
```ruby
def disable_two_factor!
transaction do
update(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
end
```
(See: `/opt/gitlab/embedded/service/gitlab-rails/app/models/user.rb`)
We can then use these methods on any user object:
```ruby
user = User.find_by(username: 'root')
user.two_factor_enabled?
user.disable_two_factor!
```
Some methods are defined by gems, or Ruby software packages, which GitLab uses.
For example, the [StateMachines](https://github.com/state-machines/state_machines-activerecord)
gem which GitLab uses to manage user state:
```ruby
state_machine :state, initial: :active do
event :block do
...
event :activate do
...
end
```
Give it a try:
```ruby
user = User.find_by(username: 'root')
user.state
user.block
user.state
user.activate
user.state
```
Earlier, we mentioned that a validation error prevents the entire object
from being saved to the database. Let's see how this can have unexpected
interactions:
```ruby
user.password = 'password'
user.password_confirmation = 'hunter2'
user.block
```
We get `false` returned! Let's find out what happened by adding a bang as we did
earlier:
```ruby
user.block!
```
Which would return:
```ruby
Traceback (most recent call last):
1: from (irb):87
StateMachines::InvalidTransition (Cannot transition state via :block from :active (Reason(s): Password confirmation doesn't match Password))
```
We see that a validation error from what feels like a completely separate
attribute comes back to haunt us when we try to update the user in any way.
In practical terms, we sometimes see this happen with GitLab administration settings --
validations are sometimes added or changed in a GitLab update, resulting in
previously saved settings now failing validation. Because you can only update
a subset of settings at once through the UI, in this case the only way to get
back to a good state is direct manipulation via Rails console.
### Commonly used Active Record models and how to look up objects
**Get a user by primary email address or username:**
```ruby
User.find_by(email: 'admin@example.com')
User.find_by(username: 'root')
```
**Get a user by primary OR secondary email address:**
```ruby
User.find_by_any_email('user@example.com')
```
The `find_by_any_email` method is a custom method added by GitLab developers rather
than a Rails-provided default method.
**Get a collection of administrator users:**
```ruby
User.admins
```
`admins` is a [scope convenience method](https://guides.rubyonrails.org/active_record_querying.html#scopes)
which does `where(admin: true)` under the hood.
**Get a project by its path:**
```ruby
Project.find_by_full_path('group/subgroup/project')
```
`find_by_full_path` is a custom method added by GitLab developers rather
than a Rails-provided default method.
**Get a project's issue or merge request by its numeric ID:**
```ruby
project = Project.find_by_full_path('group/subgroup/project')
project.issues.find_by(iid: 42)
project.merge_requests.find_by(iid: 42)
```
`iid` means "internal ID" and is how we keep issue and merge request IDs
scoped to each GitLab project.
**Get a group by its path:**
```ruby
Group.find_by_full_path('group/subgroup')
```
**Get a group's related groups:**
```ruby
group = Group.find_by_full_path('group/subgroup')
# Get a group's parent group
group.parent
# Get a group's child groups
group.children
```
**Get a group's projects:**
```ruby
group = Group.find_by_full_path('group/subgroup')
# Get group's immediate child projects
group.projects
# Get group's child projects, including those in subgroups
group.all_projects
```
**Get CI pipeline or builds:**
```ruby
Ci::Pipeline.find(4151)
Ci::Build.find(66124)
```
The pipeline and job ID numbers increment globally across your GitLab
instance, so there's no requirement to use an internal ID attribute to look them up,
unlike with issues or merge requests.
**Get the current application settings object:**
```ruby
ApplicationSetting.current
```
<!-- This redirect file can be deleted after <2022-10-05>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -450,6 +450,7 @@ Parameters:
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `approved_by_usernames` **(PREMIUM)** | string array | no | Returns merge requests which have been approved by all the users with the given `username`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. |
| `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |

View File

@ -163,3 +163,14 @@ report format XML files contain an `attachment` tag, GitLab parses the attachmen
A link to the test case attachment appears in the test case details in
[the pipeline test report](#view-unit-test-reports-on-gitlab).
## Troubleshooting
### Test report appears empty
A unit test report can appear to be empty when [viewed in a merge request](#view-unit-test-reports-on-gitlab)
if the artifact that contained the report [expires](../yaml/index.md#artifactsexpire_in).
If the artifact frequently expires too early, set a longer `expire_in` value for
the report artifact.
Alternatively, you can run a new pipeline to generate a new report.

View File

@ -240,4 +240,4 @@ For information on how to contribute documentation, see GitLab
## Getting an Enterprise Edition License
If you need a license for contributing to an EE-feature, see
[relevant information](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#for-contributors-to-the-gitlab-enterprise-edition-ee).
[relevant information](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#contributing-to-the-gitlab-enterprise-edition-ee).

View File

@ -505,16 +505,22 @@ To install and run Snowplow Micro, complete these steps to modify the
1. Set the environment variable to tell the GDK to use Snowplow Micro in development. This overrides two `application_settings` options:
- `snowplow_enabled` setting will instead return `true` from `Gitlab::Tracking.enabled?`
- `snowplow_collector_hostname` setting will instead always return `localhost:9090` (or whatever is set for `SNOWPLOW_MICRO_URI`) from `Gitlab::Tracking.collector_hostname`.
- `snowplow_collector_hostname` setting will instead always return `localhost:9090` (or whatever port is set for `snowplow_micro.port` GDK setting) from `Gitlab::Tracking.collector_hostname`.
```shell
export SNOWPLOW_MICRO_ENABLE=1
gdk config set snowplow_micro.enabled true
```
Optionally, you can set the URI for you Snowplow Micro instance as well (the default value is `http://localhost:9090`):
Optionally, you can set the port for you Snowplow Micro instance as well (the default value is `9090`):
```shell
export SNOWPLOW_MICRO_URI=https://127.0.0.1:8080
gdk config set snowplow_micro.port 8080
```
1. Regenerate the project YAML config:
```shell
gdk reconfigure
```
1. Restart GDK:

View File

@ -6,7 +6,11 @@ module API
TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :download_code, user_project }
before do
authorize! :download_code, user_project
not_found! unless user_project.repo_exists?
end
params do
requires :id, type: String, desc: 'The ID of a project'

View File

@ -44,29 +44,19 @@ module Gitlab
options
end
options[:skip_read_total_timeout] = true if options[:skip_read_total_timeout].nil? && options[:stream_body]
if options[:skip_read_total_timeout]
if options[:stream_body]
return httparty_perform_request(http_method, path, options_with_timeouts, &block)
end
start_time = nil
read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
tracked_timeout_error = false
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
start_time ||= Gitlab::Metrics::System.monotonic_time
elapsed = Gitlab::Metrics::System.monotonic_time - start_time
if elapsed > read_total_timeout
error = ReadTotalTimeout.new("Request timed out after #{elapsed} seconds")
raise error if options[:use_read_total_timeout]
unless tracked_timeout_error
Gitlab::ErrorTracking.track_exception(error)
tracked_timeout_error = true
end
raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds"
end
block.call fragment if block

View File

@ -39,7 +39,9 @@ module Gitlab
end
def snowplow_micro_enabled?
Rails.env.development? && Gitlab::Utils.to_boolean(ENV['SNOWPLOW_MICRO_ENABLE'])
Rails.env.development? && Gitlab.config.snowplow_micro.enabled
rescue Settingslogic::MissingSetting
Gitlab::Utils.to_boolean(ENV['SNOWPLOW_MICRO_ENABLE'])
end
private

View File

@ -30,8 +30,9 @@ module Gitlab
def uri
strong_memoize(:snowplow_uri) do
uri = URI(ENV['SNOWPLOW_MICRO_URI'] || DEFAULT_URI)
uri = URI("http://#{ENV['SNOWPLOW_MICRO_URI']}") unless %w[http https].include?(uri.scheme)
base = base_uri
uri = URI(base)
uri = URI("http://#{base}") unless %w[http https].include?(uri.scheme)
uri
end
end
@ -47,6 +48,14 @@ module Gitlab
def protocol
uri.scheme
end
def base_uri
url = Gitlab.config.snowplow_micro.address
scheme = Gitlab.config.gitlab.https ? 'https' : 'http'
"#{scheme}://#{url}"
rescue Settingslogic::MissingSetting
ENV['SNOWPLOW_MICRO_URI'] || DEFAULT_URI
end
end
end
end

View File

@ -52,7 +52,7 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.25.0",
"@gitlab/ui": "42.12.0",
"@gitlab/ui": "42.13.0",
"@gitlab/visual-review-tools": "1.7.3",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",

View File

@ -182,5 +182,45 @@ RSpec.describe "Group Runners" do
end
end
end
context 'when group_runner_view_ui is enabled' do
before do
stub_feature_flags(group_runner_view_ui: true)
end
it 'user views runner details' do
visit group_runner_path(group, runner)
expect(page).to have_content "#{s_('Runners|Description')} runner-foo"
end
it 'user edits the runner to be protected' do
visit edit_group_runner_path(group, runner)
expect(page.find_field('runner[access_level]')).not_to be_checked
check 'runner_access_level'
click_button _('Save changes')
expect(page).to have_content "#{s_('Runners|Configuration')} #{s_('Runners|Protected')}"
end
context 'when a runner has a tag' do
before do
runner.update!(tag_list: ['tag'])
end
it 'user edits runner not to run untagged jobs' do
visit edit_group_runner_path(group, runner)
page.find_field('runner[tag_list]').set('tag, tag2')
uncheck 'runner_run_untagged'
click_button _('Save changes')
expect(page).to have_content "#{s_('Runners|Tags')} tag tag2"
end
end
end
end
end

View File

@ -0,0 +1,197 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
import GroupRunnerShowApp from '~/runner/group_runner_show/group_runner_show_app.vue';
import { captureException } from '~/runner/sentry_utils';
import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const mockRunnersPath = '/groups/group1/-/runners';
const mockEditGroupRunnerPath = `/groups/group1/-/runners/${mockRunnerId}/edit`;
Vue.use(VueApollo);
describe('GroupRunnerShowApp', () => {
let wrapper;
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
data: {
runner: { ...mockRunner, ...runner },
},
});
};
const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(GroupRunnerShowApp, {
apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
runnersPath: mockRunnersPath,
editGroupRunnerPath: mockEditGroupRunnerPath,
...props,
},
...options,
});
return waitForPromises();
};
afterEach(() => {
mockRunnerQuery.mockReset();
wrapper.destroy();
});
describe('When showing runner details', () => {
beforeEach(async () => {
mockRunnerQueryResult();
await createComponent({ mountFn: mountExtended });
});
it('expect GraphQL ID to be requested', async () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
it('displays the header', async () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
it('displays edit, pause, delete buttons', async () => {
expect(findRunnerEditButton().exists()).toBe(true);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
it('shows basic runner details', () => {
const expected = `Description Instance runner
Last contact Never contacted
Version 1.0.0
IP Address 127.0.0.1
Executor None
Architecture None
Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
it('renders runner details component', () => {
expect(findRunnerDetails().props('runner')).toEqual(mockRunner);
});
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
...mockRunner.userPermissions,
updateRunner: false,
},
});
await createComponent({
mountFn: mountExtended,
});
});
it('does not display edit and pause buttons', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
it('displays delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(true);
});
});
describe('when runner cannot be deleted', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
...mockRunner.userPermissions,
deleteRunner: false,
},
});
await createComponent({
mountFn: mountExtended,
});
});
it('does not display delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
it('displays edit and pause buttons', () => {
expect(findRunnerEditButton().exists()).toBe(true);
expect(findRunnerPauseButton().exists()).toBe(true);
});
});
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
mountFn: mountExtended,
});
});
it('redirects to the runner list page', () => {
findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
});
});
});
describe('When there is an error', () => {
beforeEach(async () => {
mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!'));
await createComponent();
});
it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'GroupRunnerShowApp',
});
});
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalled();
});
});
});

View File

@ -1,11 +1,10 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
@ -67,34 +66,32 @@ describe('App component', () => {
const createComponent = ({ shouldShowCallout = true, ...propsData } = {}) => {
userCalloutDismissSpy = jest.fn();
wrapper = extendedWrapper(
mount(SecurityConfigurationApp, {
propsData: {
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
securityTrainingEnabled: true,
...propsData,
},
provide: {
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
projectFullPath,
vulnerabilityTrainingDocsPath,
},
stubs: {
...stubChildren(SecurityConfigurationApp),
GlLink: false,
GlSprintf: false,
LocalStorageSync: false,
SectionLayout: false,
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
},
}),
);
wrapper = mountExtended(SecurityConfigurationApp, {
propsData: {
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
securityTrainingEnabled: true,
...propsData,
},
provide: {
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
projectFullPath,
vulnerabilityTrainingDocsPath,
},
stubs: {
...stubChildren(SecurityConfigurationApp),
GlLink: false,
GlSprintf: false,
LocalStorageSync: false,
SectionLayout: false,
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
},
});
};
const findMainHeading = () => wrapper.find('h1');

View File

@ -146,7 +146,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
let(:snowplow_micro_url) { "http://#{snowplow_micro_hostname}/" }
before do
stub_env('SNOWPLOW_MICRO_ENABLE', 1)
stub_config(snowplow_micro: { enabled: true })
allow(Gitlab::Tracking).to receive(:collector_hostname).and_return(snowplow_micro_hostname)
end
@ -169,9 +169,9 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url))
end
context 'when not enabled using ENV[SNOWPLOW_MICRO_ENABLE]' do
context 'when not enabled using config' do
before do
stub_env('SNOWPLOW_MICRO_ENABLE', nil)
stub_config(snowplow_micro: { enabled: false })
end
it 'does not add Snowplow Micro URL to connect-src' do

View File

@ -15,18 +15,6 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
stub_all_dns('https://example.org', ip_address: '93.184.216.34')
end
context 'with use_read_total_timeout option' do
let(:options) { { use_read_total_timeout: true } }
it 'sets up the connection using the Gitlab::NetHttpAdapter' do
expect(connection).to be_a(Gitlab::NetHttpAdapter)
expect(connection.address).to eq('93.184.216.34')
expect(connection.hostname_override).to eq('example.org')
expect(connection.addr_port).to eq('example.org')
expect(connection.port).to eq(443)
end
end
context 'when local requests are allowed' do
let(:options) { { allow_local_requests: true } }

View File

@ -83,67 +83,25 @@ RSpec.describe Gitlab::HTTP do
subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
shared_examples 'tracks the timeout but does not raise an error' do
specify :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(Gitlab::HTTP::ReadTotalTimeout)
).once
expect { request_slow_responder }.not_to raise_error
end
it 'still calls the block' do
expect { |b| described_class.post('http://example.org', **options, &b) }.to yield_successive_args('a', 'b')
end
end
shared_examples 'does not track or raise timeout error' do
specify :aggregate_failures do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
expect { request_slow_responder }.not_to raise_error
end
end
it_behaves_like 'tracks the timeout but does not raise an error'
context 'and use_read_total_timeout option is truthy' do
let(:options) { { use_read_total_timeout: true } }
it 'raises an error' do
expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
end
it 'raises an error' do
expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
end
context 'and timeout option is greater than DEFAULT_READ_TOTAL_TIMEOUT' do
let(:options) { { timeout: 10.seconds } }
it_behaves_like 'does not track or raise timeout error'
it 'does not raise an error' do
expect { request_slow_responder }.not_to raise_error
end
end
context 'and stream_body option is truthy' do
let(:options) { { stream_body: true } }
it_behaves_like 'does not track or raise timeout error'
context 'but skip_read_total_timeout option is falsey' do
let(:options) { { stream_body: true, skip_read_total_timeout: false } }
it_behaves_like 'tracks the timeout but does not raise an error'
it 'does not raise an error' do
expect { request_slow_responder }.not_to raise_error
end
end
context 'and skip_read_total_timeout option is truthy' do
let(:options) { { skip_read_total_timeout: true } }
it_behaves_like 'does not track or raise timeout error'
end
context 'and skip_read_total_timeout option is falsely' do
let(:options) { { skip_read_total_timeout: false } }
it_behaves_like 'tracks the timeout but does not raise an error'
end
end
it 'calls a block' do

View File

@ -5,46 +5,83 @@ require 'spec_helper'
RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do
include StubENV
let(:snowplow_micro_settings) do
{
enabled: true,
address: address
}
end
let(:address) { "gdk.test:9091" }
before do
stub_application_setting(snowplow_enabled: true)
stub_env('SNOWPLOW_MICRO_ENABLE', '1')
allow(Rails.env).to receive(:development?).and_return(true)
end
describe '#hostname' do
context 'when SNOWPLOW_MICRO_URI is set' do
context 'when snowplow_micro config is set' do
let(:address) { '127.0.0.1:9091' }
before do
stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091')
stub_config(snowplow_micro: snowplow_micro_settings)
end
it 'returns hostname URI part' do
expect(subject.hostname).to eq('gdk.test:9091')
it 'returns proper URI' do
expect(subject.hostname).to eq('127.0.0.1:9091')
expect(subject.uri.scheme).to eq('http')
end
context 'when gitlab config has https scheme' do
before do
stub_config_setting(https: true)
end
it 'returns proper URI' do
expect(subject.hostname).to eq('127.0.0.1:9091')
expect(subject.uri.scheme).to eq('https')
end
end
end
context 'when SNOWPLOW_MICRO_URI is without protocol' do
context 'when snowplow_micro config is not set' do
before do
stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091')
allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting)
end
it 'returns hostname URI part' do
expect(subject.hostname).to eq('gdk.test:9091')
end
end
context 'when SNOWPLOW_MICRO_URI has scheme and port' do
before do
stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091')
end
context 'when SNOWPLOW_MICRO_URI is hostname only' do
before do
stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport')
it 'returns hostname URI part' do
expect(subject.hostname).to eq('gdk.test:9091')
end
end
it 'returns hostname URI with default HTTP port' do
expect(subject.hostname).to eq('uriwithoutport:80')
end
end
context 'when SNOWPLOW_MICRO_URI is without protocol' do
before do
stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091')
end
context 'when SNOWPLOW_MICRO_URI is not set' do
it 'returns localhost hostname' do
expect(subject.hostname).to eq('localhost:9090')
it 'returns hostname URI part' do
expect(subject.hostname).to eq('gdk.test:9091')
end
end
context 'when SNOWPLOW_MICRO_URI is hostname only' do
before do
stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport')
end
it 'returns hostname URI with default HTTP port' do
expect(subject.hostname).to eq('uriwithoutport:80')
end
end
context 'when SNOWPLOW_MICRO_URI is not set' do
it 'returns localhost hostname' do
expect(subject.hostname).to eq('localhost:9090')
end
end
end
end
@ -53,7 +90,7 @@ RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do
let_it_be(:group) { create :group }
before do
stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091')
stub_config(snowplow_micro: snowplow_micro_settings)
end
it 'includes protocol with the correct value' do

View File

@ -34,6 +34,26 @@ RSpec.describe Gitlab::Tracking do
end
end
shared_examples 'delegates to SnowplowMicro destination with proper options' do
it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro
it 'returns useful client options' do
expected_fields = {
namespace: 'gl',
hostname: 'localhost:9090',
cookieDomain: '.gitlab.com',
appId: '_abc123_',
protocol: 'http',
port: 9090,
forceSecureTracker: false,
formTracking: true,
linkClickTracking: true
}
expect(subject.options(nil)).to match(expected_fields)
end
end
context 'when destination is Snowplow' do
it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow
@ -53,26 +73,31 @@ RSpec.describe Gitlab::Tracking do
context 'when destination is SnowplowMicro' do
before do
stub_env('SNOWPLOW_MICRO_ENABLE', '1')
allow(Rails.env).to receive(:development?).and_return(true)
end
it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro
context "enabled with yml config" do
let(:snowplow_micro_settings) do
{
enabled: true,
address: "localhost:9090"
}
end
it 'returns useful client options' do
expected_fields = {
namespace: 'gl',
hostname: 'localhost:9090',
cookieDomain: '.gitlab.com',
appId: '_abc123_',
protocol: 'http',
port: 9090,
forceSecureTracker: false,
formTracking: true,
linkClickTracking: true
}
before do
stub_config(snowplow_micro: snowplow_micro_settings)
end
expect(subject.options(nil)).to match(expected_fields)
it_behaves_like 'delegates to SnowplowMicro destination with proper options'
end
context "enabled with env variable" do
before do
allow(Gitlab.config).to receive(:snowplow_micro).and_raise(Settingslogic::MissingSetting)
stub_env('SNOWPLOW_MICRO_ENABLE', '1')
end
it_behaves_like 'delegates to SnowplowMicro destination with proper options'
end
end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AwarenessSession do
subject { AwarenessSession.for(session_id) }
let(:user) { create(:user) }
let(:session_id) { 1 }
after do
redis_shared_state_cleanup!
end
describe "when a user joins a session" do
let(:presence_ttl) { 15.minutes }
it "changes number of session members" do
expect { subject.join(user) }.to change(subject, :size).by(1)
end
it "returns user as member of session with last_activity timestamp" do
freeze_time do
subject.join(user)
session_users = subject.users_with_last_activity
session_user, last_activity = session_users.first
expect(session_user.id).to be(user.id)
expect(last_activity).to be_eql(Time.now.utc)
end
end
it "reports user as present" do
freeze_time do
subject.join(user)
expect(subject.present?(user, threshold: presence_ttl)).to be true
end
end
it "reports user as away after a certain time on inactivity" do
subject.join(user)
travel_to((presence_ttl + 1.minute).from_now) do
expect(subject.away?(user, threshold: presence_ttl)).to be true
end
end
it "reports user as present still when there was some activity" do
subject.join(user)
travel_to((presence_ttl - 1.minute).from_now) do
subject.touch!(user)
end
travel_to((presence_ttl + 1.minute).from_now) do
expect(subject.present?(user, threshold: presence_ttl)).to be true
end
end
it "creates user and session awareness keys in store" do
subject.join(user)
Gitlab::Redis::SharedState.with do |redis|
keys = redis.scan_each(match: "gitlab:awareness:*").to_a
expect(keys.size).to be(2)
end
end
it "sets a timeout for user and session key" do
subject.join(user)
subject_id = Digest::SHA256.hexdigest(session_id.to_s)[0, 15]
Gitlab::Redis::SharedState.with do |redis|
ttl_session = redis.ttl("gitlab:awareness:session:#{subject_id}:users")
ttl_user = redis.ttl("gitlab:awareness:user:#{user.id}:sessions")
expect(ttl_session).to be > 0
expect(ttl_user).to be > 0
end
end
end
describe "when a user leaves a session" do
it "changes number of session members" do
subject.join(user)
expect { subject.leave(user) }.to change(subject, :size).by(-1)
end
it "destroys the session when it was the last user" do
subject.join(user)
expect { subject.leave(user) }.to change(subject, :id).to(nil)
end
end
describe "when last user leaves a session" do
it "session and user keys are removed" do
subject.join(user)
Gitlab::Redis::SharedState.with do |redis|
expect { subject.leave(user) }
.to change { redis.scan_each(match: "gitlab:awareness:*").to_a.size }
.to(0)
end
end
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Awareness do
subject { create(:user) }
let(:session) { AwarenessSession.for(1) }
after do
redis_shared_state_cleanup!
end
describe "when joining a session" do
it "increases the number of sessions" do
expect { subject.join(session) }
.to change { subject.session_ids.size }
.by(1)
end
end
describe "when leaving session" do
it "decreases the number of sessions" do
subject.join(session)
expect { subject.leave(session) }
.to change { subject.session_ids.size }
.by(-1)
end
end
describe "when joining multiple sessions" do
let(:session2) { AwarenessSession.for(2) }
it "increases number of active sessions for user" do
expect do
subject.join(session)
subject.join(session2)
end.to change { subject.session_ids.size }
.by(2)
end
end
end

View File

@ -90,6 +90,13 @@ RSpec.describe API::Tags do
let(:request) { get api(route, current_user) }
end
end
context 'when repository does not exist' do
it_behaves_like '404 response' do
let(:project) { create(:project, creator: user) }
let(:request) { get api(route, current_user) }
end
end
end
context 'when unauthenticated', 'and project is public' do

View File

@ -20,7 +20,7 @@ require (
github.com/johannesboyne/gofakes3 v0.0.0-20220627085814-c3ac35da23b2
github.com/jpillora/backoff v1.0.0
github.com/mitchellh/copystructure v1.0.0
github.com/prometheus/client_golang v1.12.1
github.com/prometheus/client_golang v1.12.2
github.com/rafaeljusto/redigomock/v3 v3.1.1
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/sirupsen/logrus v1.8.1

View File

@ -939,8 +939,9 @@ github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeD
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=

View File

@ -1053,10 +1053,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.25.0.tgz#0fb831959c9f312ebb665d23ba8944f26faea164"
integrity sha512-R2oS/VghjP1T4WSTEkbadrzencmBesortvHT8VZUgUB1uQTLg52b843rTw/atVWpW2ecFRrEbjM8/lDwUwx0Aw==
"@gitlab/ui@42.12.0":
version "42.12.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.12.0.tgz#0a8b24507bc8459dd2408c7c387fe0aa10da65c9"
integrity sha512-OowlK2U9Mcx2LdpBYAqDQ0WwYdBe7vJMSVZwWri+iaQWtJziGowyFJBMYxDebK5IoYXQAnY4rTxwNYZfdqgk1w==
"@gitlab/ui@42.13.0":
version "42.13.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.13.0.tgz#bde99885d97d06fc16fce5054b68d85799fe85e5"
integrity sha512-uYHYWQ5RlmmMFjLbLxrJnhTqEo/Hh5dLKNK7+WAyyCFke9ycn70WQ4quxY3MJckdMhNS5dYg/6DhrjqUQpFBPA==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"