Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e129eff883
commit
5875e92ecf
|
@ -1 +1 @@
|
|||
d92a2acbdcc9e20cac9e64692564556314f6e476
|
||||
bfd3175bf92587f21d17e2107e1e7e2ee0fa69bc
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { initGroupRunnerShow } from '~/runner/group_runner_show';
|
||||
|
||||
initGroupRunnerShow();
|
|
@ -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 }} </span>
|
||||
<span class="gl-text-secondary">{{ createdTime }} </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 }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-if="author" class="d-flex">
|
||||
<span class="text-secondary">{{ __('by') }} </span>
|
||||
<div v-if="author" class="gl-display-flex">
|
||||
<span class="gl-text-secondary">{{ __('by') }} </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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -155,7 +155,6 @@ module Integrations
|
|||
|
||||
query_params[:os_authType] = 'basic'
|
||||
params[:basic_auth] = basic_auth
|
||||
params[:use_read_total_timeout] = true
|
||||
params
|
||||
end
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@ class User < ApplicationRecord
|
|||
include Gitlab::SQL::Pattern
|
||||
include AfterCommitQueue
|
||||
include Avatarable
|
||||
include Awareness
|
||||
include Referable
|
||||
include Sortable
|
||||
include CaseSensitivity
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 } }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue