Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-05 15:12:53 +00:00
parent c9b0dfef1b
commit a84626f13d
90 changed files with 1386 additions and 1161 deletions

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'rails', '~> 6.1.3.2'
gem 'rails', '~> 6.1.4.1'
gem 'bootsnap', '~> 1.4.6'

View File

@ -11,63 +11,63 @@ GEM
RedCloth (4.3.2)
acme-client (2.0.6)
faraday (>= 0.17, < 2.0.0)
actioncable (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
actioncable (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailbox (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (>= 2.7.1)
actionmailer (6.1.3.2)
actionpack (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionmailer (6.1.4.1)
actionpack (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activesupport (= 6.1.4.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.3.2)
actionview (= 6.1.3.2)
activesupport (= 6.1.3.2)
actionpack (6.1.4.1)
actionview (= 6.1.4.1)
activesupport (= 6.1.4.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2)
actionpack (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
actiontext (6.1.4.1)
actionpack (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
nokogiri (>= 1.8.5)
actionview (6.1.3.2)
activesupport (= 6.1.3.2)
actionview (6.1.4.1)
activesupport (= 6.1.4.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.3.2)
activesupport (= 6.1.3.2)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
globalid (>= 0.3.6)
activemodel (6.1.3.2)
activesupport (= 6.1.3.2)
activerecord (6.1.3.2)
activemodel (= 6.1.3.2)
activesupport (= 6.1.3.2)
activemodel (6.1.4.1)
activesupport (= 6.1.4.1)
activerecord (6.1.4.1)
activemodel (= 6.1.4.1)
activesupport (= 6.1.4.1)
activerecord-explain-analyze (0.1.0)
activerecord (>= 4)
pg
activestorage (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activesupport (= 6.1.3.2)
activestorage (6.1.4.1)
actionpack (= 6.1.4.1)
activejob (= 6.1.4.1)
activerecord (= 6.1.4.1)
activesupport (= 6.1.4.1)
marcel (~> 1.0.0)
mini_mime (~> 1.0.2)
activesupport (6.1.3.2)
mini_mime (>= 1.1.0)
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -505,8 +505,8 @@ GEM
omniauth (~> 1.3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
globalid (0.4.2)
activesupport (>= 4.2.0)
globalid (0.5.2)
activesupport (>= 5.0)
gon (6.4.0)
actionpack (>= 3.0.20)
i18n (>= 0.7)
@ -746,7 +746,7 @@ GEM
mime-types-data (3.2020.0512)
mini_histogram (0.3.1)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_mime (1.1.1)
mini_portile2 (2.5.3)
minitest (5.11.3)
mixlib-cli (2.1.8)
@ -783,7 +783,7 @@ GEM
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.0.0)
netrc (0.11.0)
nio4r (2.5.4)
nio4r (2.5.8)
no_proxy_fix (0.1.2)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
@ -964,20 +964,20 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.5.2)
rails (6.1.3.2)
actioncable (= 6.1.3.2)
actionmailbox (= 6.1.3.2)
actionmailer (= 6.1.3.2)
actionpack (= 6.1.3.2)
actiontext (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activemodel (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
rails (6.1.4.1)
actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4.1)
actionpack (= 6.1.4.1)
actiontext (= 6.1.4.1)
actionview (= 6.1.4.1)
activejob (= 6.1.4.1)
activemodel (= 6.1.4.1)
activerecord (= 6.1.4.1)
activestorage (= 6.1.4.1)
activesupport (= 6.1.4.1)
bundler (>= 1.15.0)
railties (= 6.1.3.2)
railties (= 6.1.4.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -991,11 +991,11 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
railties (6.1.4.1)
actionpack (= 6.1.4.1)
activesupport (= 6.1.4.1)
method_source
rake (>= 0.8.7)
rake (>= 0.13)
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.6)
@ -1351,7 +1351,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.6.1)
websocket-driver (0.7.3)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wikicloth (0.8.1)
@ -1573,7 +1573,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.16.0)
rack-proxy (~> 0.6.0)
rack-timeout (~> 0.5.1)
rails (~> 6.1.3.2)
rails (~> 6.1.4.1)
rails-controller-testing
rails-i18n (~> 6.0)
rainbow (~> 3.0)

View File

@ -128,6 +128,12 @@ export default {
lastReleaseLink() {
return `${this.error.externalBaseUrl}/releases/${this.error.lastReleaseVersion}`;
},
firstCommitLink() {
return `${this.error.externalBaseUrl}/-/commit/${this.error.firstReleaseVersion}`;
},
lastCommitLink() {
return `${this.error.externalBaseUrl}/-/commit/${this.error.lastReleaseVersion}`;
},
showStacktrace() {
return Boolean(this.stacktrace?.length);
},
@ -394,7 +400,7 @@ export default {
<span>{{ error.gitlabIssuePath }}</span>
</gl-link>
</li>
<li>
<li v-if="!error.integrated">
<strong class="bold">{{ __('Sentry event') }}:</strong>
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(error.externalUrl)"
@ -409,15 +415,21 @@ export default {
<li v-if="error.firstReleaseVersion">
<strong class="bold">{{ __('First seen') }}:</strong>
<time-ago-tooltip :time="error.firstSeen" />
<gl-link :href="firstReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.firstReleaseVersion }}</span>
<gl-link v-if="error.integrated" :href="firstCommitLink">
{{ __('GitLab commit') }}: {{ error.firstReleaseVersion }}
</gl-link>
<gl-link v-else :href="firstReleaseLink" target="_blank">
{{ __('Release') }}: {{ error.firstReleaseVersion }}
</gl-link>
</li>
<li v-if="error.lastReleaseVersion">
<strong class="bold">{{ __('Last seen') }}:</strong>
<time-ago-tooltip :time="error.lastSeen" />
<gl-link :href="lastReleaseLink" target="_blank">
<span>{{ __('Release') }}: {{ error.lastReleaseVersion }}</span>
<gl-link v-if="error.integrated" :href="lastCommitLink">
{{ __('GitLab commit') }}: {{ error.lastReleaseVersion }}
</gl-link>
<gl-link v-else :href="lastReleaseLink" target="_blank">
{{ __('Release') }}: {{ error.lastReleaseVersion }}
</gl-link>
</li>
<li>

View File

@ -23,6 +23,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
gitlabCommit
gitlabCommitPath
gitlabIssuePath
integrated
}
}
}

View File

@ -1,9 +1,14 @@
<script>
import { GlButton, GlModal, GlSprintf, GlIcon } from '@gitlab/ui';
import { __, n__ } from '~/locale';
import { ISSUABLE_TYPE } from '../constants';
export default {
name: 'CsvExportModal',
i18n: {
exportText: __(
'The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment.',
),
},
components: {
GlButton,
GlModal,
@ -32,53 +37,39 @@ export default {
required: true,
},
},
data() {
return {
// eslint-disable-next-line @gitlab/require-i18n-strings
issuableName: this.issuableType === ISSUABLE_TYPE.issues ? 'issues' : 'merge requests',
};
computed: {
isIssue() {
return this.issuableType === ISSUABLE_TYPE.issues;
},
exportText() {
return this.isIssue ? __('Export issues') : __('Export merge requests');
},
issuableCountText() {
return this.isIssue
? n__('1 issue selected', '%d issues selected', this.issuableCount)
: n__('1 merge request selected', '%d merge requests selected', this.issuableCount);
},
},
issueableType: ISSUABLE_TYPE,
};
</script>
<template>
<gl-modal :modal-id="modalId" body-class="gl-p-0!" data-qa-selector="export_issuable_modal">
<template #modal-title>
<gl-sprintf :message="__('Export %{name}')">
<template #name>{{ issuableName }}</template>
</gl-sprintf>
</template>
<gl-modal
:modal-id="modalId"
body-class="gl-p-0!"
:title="exportText"
data-qa-selector="export_issuable_modal"
>
<div
v-if="issuableCount > -1"
class="gl-justify-content-start gl-align-items-center gl-p-4 gl-border-b-solid gl-border-1 gl-border-gray-50"
>
<gl-icon name="check" class="gl-color-green-400" />
<strong class="gl-m-3">
<gl-sprintf
v-if="issuableType === $options.issueableType.issues"
:message="n__('1 issue selected', '%d issues selected', issuableCount)"
>
<template #issuableCount>{{ issuableCount }}</template>
</gl-sprintf>
<gl-sprintf
v-else
:message="n__('1 merge request selected', '%d merge requests selected', issuableCount)"
>
<template #issuableCount>{{ issuableCount }}</template>
</gl-sprintf>
</strong>
<strong class="gl-m-3">{{ issuableCountText }}</strong>
</div>
<div class="modal-text gl-px-4 gl-py-5">
<gl-sprintf
:message="
__(
`The CSV export will be created in the background. Once finished, it will be sent to %{strongStart}${email}%{strongEnd} in an attachment.`,
)
"
>
<template #strong="{ content }">
<strong>{{ content }}</strong>
<gl-sprintf :message="$options.i18n.exportText">
<template #email>
<strong>{{ email }}</strong>
</template>
</gl-sprintf>
</div>
@ -92,9 +83,7 @@ export default {
data-track-action="click_button"
:data-track-label="`export_${issuableType}_csv`"
>
<gl-sprintf :message="__('Export %{name}')">
<template #name>{{ issuableName }}</template>
</gl-sprintf>
{{ exportText }}
</gl-button>
</template>
</gl-modal>

View File

@ -15,6 +15,8 @@ import CsvImportModal from './csv_import_modal.vue';
export default {
i18n: {
exportAsCsvButtonText: __('Export as CSV'),
importCsvText: __('Import CSV'),
importFromJiraText: __('Import from Jira'),
importIssuesText: __('Import issues'),
},
name: 'CsvImportExportButtons',
@ -101,13 +103,16 @@ export default {
:text-sr-only="!showLabel"
:icon="importButtonIcon"
>
<gl-dropdown-item v-gl-modal="importModalId">{{ __('Import CSV') }}</gl-dropdown-item>
<gl-dropdown-item v-gl-modal="importModalId">
{{ $options.i18n.importCsvText }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canEdit"
:href="projectImportJiraPath"
data-qa-selector="import_from_jira_link"
>{{ __('Import from Jira') }}</gl-dropdown-item
>
{{ $options.i18n.importFromJiraText }}
</gl-dropdown-item>
</gl-dropdown>
</gl-button-group>
<csv-export-modal

View File

@ -1,23 +1,28 @@
<script>
import { GlModal, GlSprintf, GlFormGroup, GlButton } from '@gitlab/ui';
import { GlModal, GlFormGroup } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { ISSUABLE_TYPE } from '../constants';
import { __, sprintf } from '~/locale';
export default {
name: 'CsvImportModal',
i18n: {
maximumFileSizeText: __('The maximum file size allowed is %{size}.'),
importIssuesText: __('Import issues'),
uploadCsvFileText: __('Upload CSV file'),
mainText: __(
"Your issues will be imported in the background. Once finished, you'll get a confirmation email.",
),
helpText: __(
'It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.',
),
},
actionPrimary: {
text: __('Import issues'),
},
components: {
GlModal,
GlSprintf,
GlFormGroup,
GlButton,
},
inject: {
issuableType: {
default: '',
},
exportCsvPath: {
default: '',
},
importCsvIssuesPath: {
default: '',
},
@ -31,11 +36,10 @@ export default {
required: true,
},
},
data() {
return {
// eslint-disable-next-line @gitlab/require-i18n-strings
issuableName: this.issuableType === ISSUABLE_TYPE.issues ? 'issues' : 'merge requests',
};
computed: {
maxFileSizeText() {
return sprintf(this.$options.i18n.maximumFileSizeText, { size: this.maxAttachmentSize });
},
},
methods: {
submitForm() {
@ -47,34 +51,22 @@ export default {
</script>
<template>
<gl-modal :modal-id="modalId" :title="__('Import issues')">
<gl-modal
:modal-id="modalId"
:title="$options.i18n.importIssuesText"
:action-primary="$options.actionPrimary"
@primary="submitForm"
>
<form ref="form" :action="importCsvIssuesPath" enctype="multipart/form-data" method="post">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<p>
{{
__(
"Your issues will be imported in the background. Once finished, you'll get a confirmation email.",
)
}}
</p>
<gl-form-group :label="__('Upload CSV file')" label-for="file">
<p>{{ $options.i18n.mainText }}</p>
<gl-form-group :label="$options.i18n.uploadCsvFileText" label-for="file">
<input id="file" type="file" name="file" accept=".csv,text/csv" />
</gl-form-group>
<p class="text-secondary">
{{
__(
'It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.',
)
}}
<gl-sprintf :message="__('The maximum file size allowed is %{size}.')"
><template #size>{{ maxAttachmentSize }}</template></gl-sprintf
>
{{ $options.i18n.helpText }}
{{ maxFileSizeText }}
</p>
</form>
<template #modal-footer>
<gl-button category="primary" variant="confirm" @click="submitForm">{{
__('Import issues')
}}</gl-button>
</template>
</gl-modal>
</template>

View File

@ -1,17 +0,0 @@
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
const domainCard = document.querySelector('.js-domain-cert-show');
const domainForm = document.querySelector('.js-domain-cert-inputs');
const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
if (domainReplaceButton && domainCard && domainForm) {
domainReplaceButton.addEventListener('click', () => {
domainCard.classList.add('hidden');
domainForm.classList.remove('hidden');
domainSubmitButton.removeAttribute('disabled');
});
}

View File

@ -55,16 +55,4 @@
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
}
&.floating-status-badge {
position: absolute;
right: $gl-padding-24;
bottom: $gl-padding-4;
margin-bottom: 0;
}
}
.form-control.has-floating-status-badge {
position: relative;
padding-right: 120px;
}

View File

@ -3,7 +3,7 @@ class Admin::InstanceReviewController < Admin::ApplicationController
feature_category :devops_reports
def index
redirect_to("#{::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/instance_review?#{instance_review_params}")
redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}")
end
def instance_review_params

View File

@ -1,78 +0,0 @@
# frozen_string_literal: true
class Admin::Serverless::DomainsController < Admin::ApplicationController
before_action :check_feature_flag
before_action :domain, only: [:update, :verify, :destroy]
feature_category :not_owned
def index
@domain = PagesDomain.instance_serverless.first_or_initialize
end
def create
if PagesDomain.instance_serverless.exists?
return redirect_to admin_serverless_domains_path, notice: _('An instance-level serverless domain already exists.')
end
@domain = PagesDomain.instance_serverless.create(create_params)
if @domain.persisted?
redirect_to admin_serverless_domains_path, notice: _('Domain was successfully created.')
else
render 'index'
end
end
def update
if domain.update(update_params)
redirect_to admin_serverless_domains_path, notice: _('Domain was successfully updated.')
else
render 'index'
end
end
def destroy
if domain.serverless_domain_clusters.exists?
return redirect_to admin_serverless_domains_path,
status: :conflict,
notice: _('Domain cannot be deleted while associated to one or more clusters.')
end
domain.destroy!
redirect_to admin_serverless_domains_path,
status: :found,
notice: _('Domain was successfully deleted.')
end
def verify
result = VerifyPagesDomainService.new(domain).execute
if result[:status] == :success
flash[:notice] = _('Successfully verified domain ownership')
else
flash[:alert] = _('Failed to verify domain ownership')
end
redirect_to admin_serverless_domains_path
end
private
def domain
@domain = PagesDomain.instance_serverless.find(params[:id])
end
def check_feature_flag
render_404 unless Feature.enabled?(:serverless_domain)
end
def update_params
params.require(:pages_domain).permit(:user_provided_certificate, :user_provided_key)
end
def create_params
params.require(:pages_domain).permit(:domain, :user_provided_certificate, :user_provided_key)
end
end

View File

@ -5,11 +5,15 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
include DependencyProxy::GroupAccess
include SendFileUpload
include ::PackagesHelper # for event tracking
include WorkhorseRequest
before_action :ensure_group
before_action :ensure_token_granted!
before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :ensure_feature_enabled!
before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob]
skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob]
attr_reader :token
feature_category :dependency_proxy
@ -38,6 +42,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def blob
return blob_via_workhorse if Feature.enabled?(:dependency_proxy_workhorse, group, default_enabled: :yaml)
result = DependencyProxy::FindOrCreateBlobService
.new(group, image, token, params[:sha]).execute
@ -50,11 +56,47 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
end
def authorize_upload_blob
set_workhorse_internal_api_content_type
render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false)
end
def upload_blob
@group.dependency_proxy_blobs.create!(
file_name: blob_file_name,
file: params[:file],
size: params[:file].size
)
event_name = tracking_event_name(object_type: :blob, from_cache: false)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
head :ok
end
private
def blob_via_workhorse
blob = @group.dependency_proxy_blobs.find_by_file_name(blob_file_name)
if blob.present?
event_name = tracking_event_name(object_type: :blob, from_cache: true)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
send_upload(blob.file)
else
send_dependency(token, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
end
end
def blob_file_name
@blob_file_name ||= params[:sha].sub('sha256:', '') + '.gz'
end
def group
strong_memoize(:group) do
Group.find_by_full_path(params[:group_id], follow_redirects: request.get?)
Group.find_by_full_path(params[:group_id], follow_redirects: true)
end
end

View File

@ -13,6 +13,9 @@ module Types
field :id, GraphQL::Types::ID,
null: false,
description: 'ID (global ID) of the error.'
field :integrated, GraphQL::Types::Boolean,
null: true,
description: 'Error tracking backend.'
field :sentry_id, GraphQL::Types::String,
method: :id,
null: false,

View File

@ -41,6 +41,15 @@ module WorkhorseHelper
head :ok
end
def send_dependency(token, url, filename)
headers.store(*Gitlab::Workhorse.send_dependency(token, url))
headers['Content-Disposition'] =
ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
headers['Content-Type'] = 'application/gzip'
head :ok
end
def set_workhorse_internal_api_content_type
headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end

View File

@ -7,6 +7,14 @@ class ErrorTracking::Error < ApplicationRecord
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
has_one :first_event,
-> { order(id: :asc) },
class_name: 'ErrorTracking::ErrorEvent'
has_one :last_event,
-> { order(id: :desc) },
class_name: 'ErrorTracking::ErrorEvent'
scope :for_status, -> (status) { where(status: status) }
validates :project, presence: true
@ -90,7 +98,10 @@ class ErrorTracking::Error < ApplicationRecord
status: status,
tags: { level: nil, logger: nil },
external_url: external_url,
external_base_url: external_base_url
external_base_url: external_base_url,
integrated: true,
first_release_version: first_event&.release,
last_release_version: last_event&.release
)
end
@ -106,6 +117,6 @@ class ErrorTracking::Error < ApplicationRecord
# For compatibility with sentry integration
def external_base_url
Gitlab::Routing.url_helpers.root_url
Gitlab::Routing.url_helpers.project_url(project)
end
end

View File

@ -22,6 +22,10 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
)
end
def release
payload.dig('release')
end
private
def build_stacktrace

View File

@ -276,7 +276,7 @@ class Group < Namespace
def dependency_proxy_image_prefix
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
url = "#{web_url.downcase}#{DependencyProxy::URL_SUFFIX}"
url = "#{Gitlab::Routing.url_helpers.group_url(self).downcase}#{DependencyProxy::URL_SUFFIX}"
# Docker images do not include the protocol
url.partition('//').last

View File

@ -355,8 +355,6 @@ class Note < ApplicationRecord
end
def noteable_author?(noteable)
return false unless ::Feature.enabled?(:show_author_on_note, project)
noteable.author == self.author
end

View File

@ -2655,10 +2655,6 @@ class Project < ApplicationRecord
ProjectStatistics.increment_statistic(self, statistic, delta)
end
def merge_requests_author_approval
!!read_attribute(:merge_requests_author_approval)
end
def ci_forward_deployment_enabled?
return false unless ci_cd_settings

View File

@ -136,13 +136,11 @@ module Projects
def validate_outdated_sha!
return if latest?
if Feature.enabled?(:pages_smart_check_outdated_sha, project, default_enabled: :yaml)
# use pipeline_id in case the build is retried
last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
# use pipeline_id in case the build is retried
last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
return unless last_deployed_pipeline_id
return if last_deployed_pipeline_id <= build.pipeline_id
end
return unless last_deployed_pipeline_id
return if last_deployed_pipeline_id <= build.pipeline_id
raise InvalidStateError, 'build SHA is outdated for this ref'
end

View File

@ -7,6 +7,14 @@ module Users
end
def execute
@params = {
user_id: params.fetch(:user_id),
credit_card_validated_at: params.fetch(:credit_card_validated_at),
expiration_date: get_expiration_date(params),
last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
holder_name: params.fetch(:credit_card_holder_name)
}
::Users::CreditCardValidation.upsert(@params)
ServiceResponse.success(message: 'CreditCardValidation was set')
@ -16,5 +24,14 @@ module Users
Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
end
private
def get_expiration_date(params)
year = params.fetch(:credit_card_expiration_year)
month = params.fetch(:credit_card_expiration_month)
Date.new(year, month, -1) # last day of the month
end
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
class DependencyProxy::FileUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
before :cache, :set_content_type

View File

@ -1,99 +0,0 @@
- form_name = 'js-serverless-domain-settings'
- form_url = @domain.persisted? ? admin_serverless_domain_path(@domain.id, anchor: form_name) : admin_serverless_domains_path(anchor: form_name)
- show_certificate_card = @domain.persisted? && @domain.errors.blank?
= form_for @domain, url: form_url, html: { class: 'fieldset-form' } do |f|
= form_errors(@domain)
%fieldset
- if @domain.persisted?
- dns_record = "*.#{@domain.domain} CNAME #{Settings.pages.host}."
- verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
.form-group.row
.col-sm-6.position-relative
= f.label :domain, _('Domain'), class: 'label-bold'
= f.text_field :domain, class: 'form-control has-floating-status-badge', readonly: true
.status-badge.floating-status-badge
- text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
.badge{ class: status }
= text
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "gl-button btn has-tooltip", title: _("Retry verification")
.col-sm-6
= f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
.input-group
= text_field_tag :serverless_domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#serverless_domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
.col-sm-12.form-text.text-muted
= _("To access this domain create a new DNS record")
.form-group
= f.label :serverless_domain_verification, _('Verification status'), class: 'label-bold'
.input-group
= text_field_tag :serverless_domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#serverless_domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
= _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration.").html_safe % { link_to_help: link_to_help }
- else
.form-group
= f.label :domain, _('Domain'), class: 'label-bold'
= f.text_field :domain, class: 'form-control'
- if show_certificate_card
.card.js-domain-cert-show
.card-header
= _('Certificate')
.d-flex.justify-content-between.align-items-center.p-3
%span
= @domain.subject || _('missing')
%button.gl-button.btn.btn-danger.btn-sm.js-domain-cert-replace-btn{ type: 'button' }
= _('Replace')
.js-domain-cert-inputs{ class: ('hidden' if show_certificate_card) }
.form-group
= f.label :user_provided_certificate, _('Certificate (PEM)'), class: 'label-bold'
= f.text_area :user_provided_certificate, rows: 5, class: 'form-control', value: ''
%span.form-text.text-muted
= _("Upload a certificate for your domain with all intermediates")
.form-group
= f.label :user_provided_key, _('Key (PEM)'), class: 'label-bold'
= f.text_area :user_provided_key, rows: 5, class: 'form-control', value: ''
%span.form-text.text-muted
= _("Upload a private key for your certificate")
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "gl-button btn btn-confirm js-serverless-domain-submit", disabled: @domain.persisted?
- if @domain.persisted?
%button.gl-button.btn.btn-danger{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
= _('Delete domain')
-# haml-lint:disable NoPlainNodes
- if @domain.persisted?
- domain_attached = @domain.serverless_domain_clusters.count > 0
.modal{ id: "modal-delete-domain", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h3.page-title= _('Delete serverless domain?')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": "true" } &times;
.modal-body
- if domain_attached
= _("You must disassociate %{domain} from all clusters it is attached to before deletion.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
- else
= _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }
= _('Cancel')
= link_to _('Delete domain'),
admin_serverless_domain_path(@domain.id),
title: _('Delete'),
method: :delete,
class: "gl-button btn btn-danger",
disabled: domain_attached

View File

@ -1,25 +0,0 @@
- breadcrumb_title _("Operations")
- page_title _("Operations")
- @content_class = "limit-container-width" unless fluid_layout
-# normally expanded_by_default? is used here, but since this is the only panel
-# in this settings page, let's leave it always open by default
- expanded = true
%section.settings.as-serverless-domain.no-animate#js-serverless-domain-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Serverless domain')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set an instance-wide domain that will be available to all clusters when installing Knative.')
.settings-content
- if Gitlab.config.pages.enabled
= render 'form'
- else
.card
.card-header
= s_('GitLabPages|Domains')
.nothing-here-block
= s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.")

View File

@ -257,11 +257,6 @@
= link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do
%span
= _('CI/CD')
- if Feature.enabled?(:serverless_domain)
= nav_link(path: 'application_settings#operations') do
= link_to admin_serverless_domains_path, title: _('Operations') do
%span
= _('Operations')
= nav_link(path: 'application_settings#reporting') do
= link_to reporting_admin_application_settings_path, title: _('Reporting') do
%span

View File

@ -1,8 +1,8 @@
---
name: pages_smart_check_outdated_sha
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67303
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336574
milestone: '14.2'
name: dependency_proxy_workhorse
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68157
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339639
milestone: '14.3'
type: development
group: group::release
group: group::source code
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: serverless_domain
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21222
rollout_issue_url:
milestone: '12.8'
type: development
group: group::configure
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: show_author_on_note
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40198
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/250282
milestone: '13.4'
type: development
group: group::project management
default_enabled: false

View File

@ -96,7 +96,7 @@ module ActiveRecord
end
end
def build_arel(aliases)
def build_arel(aliases = nil)
arel = super
build_with(arel) if @values[:with]

View File

@ -38,14 +38,6 @@ namespace :admin do
resources :abuse_reports, only: [:index, :destroy]
resources :gitaly_servers, only: [:index]
namespace :serverless do
resources :domains, only: [:index, :create, :update, :destroy] do
member do
post '/verify', to: 'domains#verify'
end
end
end
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham

View File

@ -146,5 +146,7 @@ scope format: false do
constraints image: Gitlab::PathRegex.container_image_regex, sha: Gitlab::PathRegex.container_image_blob_sha_regex do
get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
get 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha' => 'groups/dependency_proxy_for_containers#blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload/authorize' => 'groups/dependency_proxy_for_containers#authorize_upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload' => 'groups/dependency_proxy_for_containers#upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
end
end

View File

@ -1090,7 +1090,7 @@ Performance bar statistics (currently only duration of SQL queries) are recorded
in that file. For example:
```json
{"severity":"INFO","time":"2020-12-04T09:29:44.592Z","correlation_id":"33680b1490ccd35981b03639c406a697","filename":"app/models/ci/pipeline.rb","method_path":"app/models/ci/pipeline.rb:each_with_object","request_id":"rYHomD0VJS4","duration_ms":26.889,"count":2,"type": "sql"}
{"severity":"INFO","time":"2020-12-04T09:29:44.592Z","correlation_id":"33680b1490ccd35981b03639c406a697","filename":"app/models/ci/pipeline.rb","method_path":"app/models/ci/pipeline.rb:each_with_object","request_id":"rYHomD0VJS4","duration_ms":26.889,"count":2,"query_type": "active-record"}
```
These statistics are logged on .com only, disabled on self-deployments.

View File

@ -13901,6 +13901,7 @@ A Sentry error.
| <a id="sentrydetailederrorgitlabcommitpath"></a>`gitlabCommitPath` | [`String`](#string) | Path to the GitLab page for the GitLab commit attributed to the error. |
| <a id="sentrydetailederrorgitlabissuepath"></a>`gitlabIssuePath` | [`String`](#string) | URL of GitLab Issue. |
| <a id="sentrydetailederrorid"></a>`id` | [`ID!`](#id) | ID (global ID) of the error. |
| <a id="sentrydetailederrorintegrated"></a>`integrated` | [`Boolean`](#boolean) | Error tracking backend. |
| <a id="sentrydetailederrorlastreleaselastcommit"></a>`lastReleaseLastCommit` | [`String`](#string) | Commit the error was last seen. |
| <a id="sentrydetailederrorlastreleaseshortversion"></a>`lastReleaseShortVersion` | [`String`](#string) | Release short version the error was last seen. |
| <a id="sentrydetailederrorlastreleaseversion"></a>`lastReleaseVersion` | [`String`](#string) | Release version the error was last seen. |

View File

@ -1091,47 +1091,51 @@ However, they should be used sparingly because:
- They are difficult and expensive to localize.
- They cannot be read by screen readers.
If you do include an image in the documentation, ensure it provides value.
Don't use `lorem ipsum` text. Try to replicate how the feature would be
used in a real-world scenario, and [use realistic text](#fake-user-information).
When needed, use images to help the reader understand:
- Where they are in a complicated process.
- How they should interact with the application.
### Capture the image
Use images to help the reader understand where they are in a process, or how
they need to interact with the application.
When you take screenshots:
- **Capture the most relevant area of the page.** Don't include unnecessary white
space or areas of the page that don't help illustrate the point. The left
sidebar of the GitLab user interface can change, so don't include the sidebar
if it's not necessary.
- **Ensure it provides value.** Don't use `lorem ipsum` text.
Try to replicate how the feature would be used in a real-world scenario, and
[use realistic text](#fake-user-information).
- **Capture only the relevant UI.** Don't include unnecessary white
space or areas of the UI that don't help illustrate the point. The
sidebars in GitLab can change, so don't include
them in screenshots unless absolutely necessary.
- **Keep it small.** If you don't need to show the full width of the screen, don't.
A value of 1000 pixels is a good maximum width for your screenshot image.
Reduce the size of your browser window as much as possible to keep elements close
together and reduce empty space. Try to keep the screenshot dimensions as small as possible.
- **Review how the image renders on the page.** Preview the image locally or use the
review app in the merge request. Make sure the image isn't blurry or overwhelming.
- **Be consistent.** Coordinate screenshots with the other screenshots already on
a documentation page. For example, if other screenshots include the left
sidebar, include the sidebar in all screenshots.
a documentation page for a consistent reading experience.
### Save the image
- Resize any wide or tall screenshots if needed, but make sure the screenshot is
still clear after being resized and compressed.
- All images **must** be [compressed](#compress-images) to 100KB or less.
In many cases, 25-50KB or less is often possible without reducing image quality.
- Save the image with a lowercase filename that's descriptive of the feature
or concept in the image. If the image is of the GitLab interface, append the
GitLab version to the filename, based on this format:
`image_name_vX_Y.png`. For example, for a screenshot taken from the pipelines
page of GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're adding an
illustration that doesn't include parts of the user interface, add the release
number corresponding to the release the image was added to; for an MR added to
11.1's milestone, a valid name for an illustration is `devops_diagram_v11_1.png`.
or concept in the image:
- If the image is of the GitLab interface, append the GitLab version to the filename,
based on this format: `image_name_vX_Y.png`. For example, for a screenshot taken
from the pipelines page of GitLab 11.1, a valid name is `pipelines_v11_1.png`.
- If you're adding an illustration that doesn't include parts of the user interface,
add the release number corresponding to the release the image was added to.
For an MR added to 11.1's milestone, a valid name for an illustration is `devops_diagram_v11_1.png`.
- Place images in a separate directory named `img/` in the same directory where
the `.md` document that you're working on is located.
- Consider using PNG images instead of JPEG.
- [Compress all PNG images](#compress-images).
- Compress GIFs with <https://ezgif.com/optimize> or similar tool.
- Images should be used (only when necessary) to illustrate the description
of a process, not to replace it.
- Max image size: 100KB (GIFs included).
- See also how to link and embed [videos](#videos) to illustrate the
documentation.
- See also how to link and embed [videos](#videos) to illustrate the documentation.
### Add the image link to content
@ -1152,8 +1156,11 @@ known tool is [`pngquant`](https://pngquant.org/), which is cross-platform and
open source. Install it by visiting the official website and following the
instructions for your OS.
If you use macOS and want all screenshots to be compressed automatically, read
[One simple trick to make your screenshots 80% smaller](https://about.gitlab.com/blog/2020/01/30/simple-trick-for-smaller-screenshots/).
GitLab has a [Ruby script](https://gitlab.com/gitlab-org/gitlab/-/blob/master/bin/pngquant)
that you can use to automate the process. In the root directory of your local
that you can use to simplify the manual process. In the root directory of your local
copy of `https://gitlab.com/gitlab-org/gitlab`, run in a terminal:
- Before compressing, if you want, check that all documentation PNG images have

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, concepts
---
# Merge request approval settings **(FREE)**
# Merge request approval settings **(PREMIUM)**
You can configure the settings for [merge request approvals](index.md) to
ensure the approval rules meet your use case. You can also configure
@ -30,7 +30,7 @@ In this section of general settings, you can configure the following settings:
| [Require user password to approve](#require-user-password-to-approve) | Force potential approvers to first authenticate with a password. |
| [Remove all approvals when commits are added to the source branch](#remove-all-approvals-when-commits-are-added-to-the-source-branch) | When enabled, remove all existing approvals on a merge request when more changes are added to it. |
## Prevent approval by author **(PREMIUM)**
## Prevent approval by author
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3349) in GitLab 11.3.
> - Moved to GitLab Premium in 13.9.
@ -52,7 +52,7 @@ this setting, unless you configure one of these options:
at the instance level, you can't edit this setting at the project or individual
merge request levels.
## Prevent approvals by users who add commits **(PREMIUM)**
## Prevent approvals by users who add commits
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10441) in GitLab 11.10.
> - Moved to GitLab Premium in 13.9.
@ -126,13 +126,25 @@ merge request could introduce a vulnerability.
To learn more, see [Security approvals in merge requests](../../../application_security/index.md#security-approvals-in-merge-requests).
## Code coverage check approvals **(PREMIUM)**
## Code coverage check approvals
You can require specific approvals if a merge request would result in a decline in code test
coverage.
To learn more, see [Coverage check approval rule](../../../../ci/pipelines/settings.md#coverage-check-approval-rule).
## Merge request approval settings cascading
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285410) in GitLab 14.4. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../../../administration/feature_flags.md), disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the `group_merge_request_approval_settings_feature_flag` flag](../../../../administration/feature_flags.md). On GitLab.com, this feature is not available.
You should not use this feature for production environments
You can now enforce merge request approval settings at an instance level which will apply to all groups on an instance and, by extension, all projects. It is also possible to enforce merge request approval settings on an individual root group which will apply to all subgroups and projects.
If the settings are inherited by a group or project, they cannot be overridden by the group or project that inherited them.
## Related links
- [Instance-level merge request approval settings](../../../admin_area/merge_requests_approvals.md)

View File

@ -16,7 +16,7 @@ module API
options: { only_owned: true, limit: projects_limit }
).execute
Entities::Project.prepare_relation(projects)
Entities::Project.prepare_relation(projects, options)
end
expose :shared_projects, using: Entities::Project do |group, options|
@ -26,7 +26,7 @@ module API
options: { only_shared: true, limit: projects_limit }
).execute
Entities::Project.prepare_relation(projects)
Entities::Project.prepare_relation(projects, options)
end
def projects_limit

View File

@ -92,7 +92,7 @@ module API
projects, options = with_custom_attributes(projects, options)
present options[:with].prepare_relation(projects), options
present options[:with].prepare_relation(projects, options), options
end
def present_groups(params, groups)

View File

@ -182,8 +182,6 @@ module API
[options[:with].prepare_relation(projects, options), options]
end
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(records, current_user).execute if current_user
present records, options
end

View File

@ -12,6 +12,8 @@ module API
preload_repository_cache(projects_relation)
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_relation, options[:current_user]).execute if options[:current_user]
projects_relation
end

View File

@ -1058,6 +1058,10 @@ module API
params do
requires :user_id, type: String, desc: 'The ID or username of the user'
requires :credit_card_validated_at, type: DateTime, desc: 'The time when the user\'s credit card was validated'
requires :credit_card_expiration_month, type: Integer, desc: 'The month the credit card expires'
requires :credit_card_expiration_year, type: Integer, desc: 'The year the credit card expires'
requires :credit_card_holder_name, type: String, desc: 'The credit card holder name'
requires :credit_card_mask_number, type: String, desc: 'The last 4 digits of credit card number'
end
put ":user_id/credit_card_validation", feature_category: :users do
authenticated_as_admin!

View File

@ -167,7 +167,8 @@ module ErrorTracking
first_release_version: issue.dig('firstRelease', 'version'),
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
last_release_short_version: issue.dig('lastRelease', 'shortVersion'),
last_release_version: issue.dig('lastRelease', 'version')
last_release_version: issue.dig('lastRelease', 'version'),
integrated: false
})
end

View File

@ -22,6 +22,7 @@ module Gitlab
:gitlab_issue,
:gitlab_project,
:id,
:integrated,
:last_release_last_commit,
:last_release_short_version,
:last_release_version,

View File

@ -158,6 +158,7 @@ module Gitlab
::Gitlab.config.uploads.storage_path,
::JobArtifactUploader.workhorse_upload_path,
::LfsObjectUploader.workhorse_upload_path,
::DependencyProxy::FileUploader.workhorse_upload_path,
File.join(Rails.root, 'public/uploads/tmp')
] + package_allowed_paths
end

View File

@ -9,6 +9,8 @@ module Gitlab
ee/lib/ee/peek
lib/peek
lib/gitlab/database
lib/gitlab/gitaly_client/call.rb
lib/gitlab/instrumentation/redis_interceptor.rb
].freeze
def initialize(redis)
@ -19,7 +21,9 @@ module Gitlab
data = request(id)
return unless data
log_sql_queries(id, data)
log_queries(id, data, 'active-record')
log_queries(id, data, 'gitaly')
log_queries(id, data, 'redis')
rescue StandardError => err
logger.error(message: "failed to process request id #{id}: #{err.message}")
end
@ -32,15 +36,17 @@ module Gitlab
Gitlab::Json.parse(json_data)
end
def log_sql_queries(id, data)
queries_by_location(data).each do |location, queries|
def log_queries(id, data, type)
json_path = ['data', type, 'details']
queries_by_location(data, json_path).each do |location, queries|
next unless location
duration = queries.sum { |query| query['duration'].to_f }
log_info = {
method_path: "#{location[:filename]}:#{location[:method]}",
filename: location[:filename],
type: :sql,
query_type: type,
request_id: id,
count: queries.count,
duration_ms: duration
@ -50,8 +56,8 @@ module Gitlab
end
end
def queries_by_location(data)
return [] unless queries = data.dig('data', 'active-record', 'details')
def queries_by_location(data, path)
return [] unless queries = data.dig(*path)
queries.group_by do |query|
parse_backtrace(query['backtrace'])

View File

@ -32,6 +32,10 @@ module Gitlab
Gitlab::ApplicationContext.current_context_attribute('meta.feature_category') || :not_owned
end
def feature_category_not_owned?
true
end
def get_worker_context
nil
end

View File

@ -15,7 +15,19 @@ module Gitlab
context_for_args = worker_class.context_for_arguments(job['args'])
wrap_in_optional_context(context_for_args, &block)
wrap_in_optional_context(context_for_args) do
# This should be inside the context for the arguments so
# that we don't override the feature category on the worker
# with the one from the caller.
#
# We do not want to set anything explicitly in the context
# when the feature category is 'not_owned'.
if worker_class.feature_category_not_owned?
yield
else
Gitlab::ApplicationContext.with_context(feature_category: worker_class.get_feature_category.to_s, &block)
end
end
end
end
end

View File

@ -38,6 +38,26 @@ module Gitlab
"#{self.subscriptions_url}/plans"
end
def self.subscriptions_gitlab_plans_url
"#{self.subscriptions_url}/gitlab_plans"
end
def self.subscriptions_instance_review_url
"#{self.subscriptions_url}/instance_review"
end
def self.add_extra_seats_url(group_id)
"#{self.subscriptions_url}/gitlab/namespaces/#{group_id}/extra_seats"
end
def self.upgrade_subscription_url(group_id, plan_id)
"#{self.subscriptions_url}/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}"
end
def self.renew_subscription_url(group_id)
"#{self.subscriptions_url}/gitlab/namespaces/#{group_id}/renew"
end
def self.subscription_portal_admin_email
ENV.fetch('SUBSCRIPTION_PORTAL_ADMIN_EMAIL', 'gl_com_api@gitlab.com')
end

View File

@ -169,6 +169,18 @@ module Gitlab
]
end
def send_dependency(token, url)
params = {
'Header' => { Authorization: ["Bearer #{token}"] },
'Url' => url
}
[
SEND_DATA_HEADER,
"send-dependency:#{encode(params)}"
]
end
def channel_websocket(channel)
details = {
'Channel' => {

View File

@ -27,7 +27,7 @@ module Sidebars
override :sprite_icon
def sprite_icon
'environment'
'deployments'
end
private

View File

@ -2012,9 +2012,6 @@ msgstr ""
msgid "Add deploy keys to grant read/write access to this repository. %{link_start}What are deploy keys?%{link_end}"
msgstr ""
msgid "Add domain"
msgstr ""
msgid "Add email address"
msgstr ""
@ -3875,9 +3872,6 @@ msgstr ""
msgid "An example showing how to use Jsonnet with GitLab dynamic child pipelines"
msgstr ""
msgid "An instance-level serverless domain already exists."
msgstr ""
msgid "An issue already exists"
msgstr ""
@ -10956,9 +10950,6 @@ msgstr ""
msgid "Delete corpus"
msgstr ""
msgid "Delete domain"
msgstr ""
msgid "Delete file"
msgstr ""
@ -10986,9 +10977,6 @@ msgstr ""
msgid "Delete self monitoring project"
msgstr ""
msgid "Delete serverless domain?"
msgstr ""
msgid "Delete snippet"
msgstr ""
@ -12097,18 +12085,6 @@ msgstr ""
msgid "Domain Name"
msgstr ""
msgid "Domain cannot be deleted while associated to one or more clusters."
msgstr ""
msgid "Domain was successfully created."
msgstr ""
msgid "Domain was successfully deleted."
msgstr ""
msgid "Domain was successfully updated."
msgstr ""
msgid "Don't have an account yet?"
msgstr ""
@ -13875,9 +13851,6 @@ msgstr ""
msgid "Export"
msgstr ""
msgid "Export %{name}"
msgstr ""
msgid "Export %{requirementsCount} requirements?"
msgstr ""
@ -13890,6 +13863,12 @@ msgstr ""
msgid "Export group"
msgstr ""
msgid "Export issues"
msgstr ""
msgid "Export merge requests"
msgstr ""
msgid "Export project"
msgstr ""
@ -14208,9 +14187,6 @@ msgstr ""
msgid "Failed to upload object map file"
msgstr ""
msgid "Failed to verify domain ownership"
msgstr ""
msgid "Failure"
msgstr ""
@ -23920,9 +23896,6 @@ msgstr ""
msgid "Operation timed out. Check pod logs for %{pod_name} for more details."
msgstr ""
msgid "Operations"
msgstr ""
msgid "Operations Dashboard"
msgstr ""
@ -30826,9 +30799,6 @@ msgstr ""
msgid "Serverless"
msgstr ""
msgid "Serverless domain"
msgstr ""
msgid "Serverless platform"
msgstr ""
@ -30985,9 +30955,6 @@ msgstr ""
msgid "Set access permissions for this token."
msgstr ""
msgid "Set an instance-wide domain that will be available to all clusters when installing Knative."
msgstr ""
msgid "Set any rate limit to %{code_open}0%{code_close} to disable the limit."
msgstr ""
@ -32792,9 +32759,6 @@ msgstr ""
msgid "Successfully updated %{last_updated_timeago}."
msgstr ""
msgid "Successfully verified domain ownership"
msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr ""
@ -33666,6 +33630,9 @@ msgstr[1] ""
msgid "The API key used by GitLab for accessing the Spam Check service endpoint."
msgstr ""
msgid "The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment."
msgstr ""
msgid "The GitLab subscription service (customers.gitlab.com) is currently experiencing an outage. You can monitor the status and get updates at %{linkStart}status.gitlab.com%{linkEnd}."
msgstr ""
@ -38608,9 +38575,6 @@ msgstr ""
msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr ""
msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
msgstr ""
msgid "You are about to permanently delete this project"
msgstr ""
@ -39034,9 +38998,6 @@ msgstr ""
msgid "You must be logged in to search across all of GitLab"
msgstr ""
msgid "You must disassociate %{domain} from all clusters it is attached to before deletion."
msgstr ""
msgid "You must have developer or higher permissions in the associated project to view job logs when debug trace is enabled. To disable debug trace, set the 'CI_DEBUG_TRACE' variable to 'false' in your pipeline configuration or CI/CD settings. If you need to view this job log, a project maintainer must add you to the project with developer permissions or higher."
msgstr ""

View File

@ -59,8 +59,8 @@
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "32.14.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.3-2",
"@rails/ujs": "6.1.3-2",
"@rails/actioncable": "6.1.4-1",
"@rails/ujs": "6.1.4-1",
"@sentry/browser": "5.30.0",
"@sourcegraph/code-host-integration": "0.0.60",
"@tiptap/core": "^2.0.0-beta.116",

View File

@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.3.2' # This should stay in sync with the root's Gemfile
gem 'activesupport', '~> 6.1.4.1' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.15.0'
gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23'

View File

@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
abstract_type (0.0.7)
activesupport (6.1.3.2)
activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -239,7 +239,7 @@ PLATFORMS
ruby
DEPENDENCIES
activesupport (~> 6.1.3.2)
activesupport (~> 6.1.4.1)
airborne (~> 0.3.4)
allure-rspec (~> 2.15.0)
capybara (~> 3.35.0)

View File

@ -6,7 +6,7 @@ RSpec.describe Admin::InstanceReviewController do
include UsageDataHelpers
let(:admin) { create(:admin) }
let(:subscriptions_url) { ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL }
let(:subscriptions_instance_review_url) { Gitlab::SubscriptionPortal.subscriptions_instance_review_url }
before do
sign_in(admin)
@ -44,7 +44,7 @@ RSpec.describe Admin::InstanceReviewController do
notes_count: 0
} }.to_query
expect(response).to redirect_to("#{subscriptions_url}/instance_review?#{params}")
expect(response).to redirect_to("#{subscriptions_instance_review_url}?#{params}")
end
end
@ -61,7 +61,7 @@ RSpec.describe Admin::InstanceReviewController do
version: ::Gitlab::VERSION
} }.to_query
expect(response).to redirect_to("#{subscriptions_url}/instance_review?#{params}")
expect(response).to redirect_to("#{subscriptions_instance_review_url}?#{params}")
end
end
end

View File

@ -1,370 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::Serverless::DomainsController do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
describe '#index' do
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'admin user' do
before do
create(:pages_domain)
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when instance-level serverless domain exists' do
let!(:serverless_domain) { create(:pages_domain, :instance_serverless) }
it 'loads the instance serverless domain' do
get :index
expect(assigns(:domain).id).to eq(serverless_domain.id)
end
end
context 'when domain does not exist' do
it 'initializes an instance serverless domain' do
get :index
domain = assigns(:domain)
expect(domain.persisted?).to eq(false)
expect(domain.wildcard).to eq(true)
expect(domain.scope).to eq('instance')
expect(domain.usage).to eq('serverless')
end
end
end
end
describe '#create' do
let(:create_params) do
sample_domain = build(:pages_domain)
{
domain: 'serverless.gitlab.io',
user_provided_certificate: sample_domain.certificate,
user_provided_key: sample_domain.key
}
end
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
post :create, params: { pages_domain: create_params }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'admin user' do
before do
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
post :create, params: { pages_domain: create_params }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when an instance-level serverless domain exists' do
let!(:serverless_domain) { create(:pages_domain, :instance_serverless) }
it 'does not create a new domain' do
expect { post :create, params: { pages_domain: create_params } }.not_to change { PagesDomain.instance_serverless.count }
end
it 'redirects to index' do
post :create, params: { pages_domain: create_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('An instance-level serverless domain already exists.')
end
end
context 'when an instance-level serverless domain does not exist' do
it 'creates an instance serverless domain with the provided attributes' do
expect { post :create, params: { pages_domain: create_params } }.to change { PagesDomain.instance_serverless.count }.by(1)
domain = PagesDomain.instance_serverless.first
expect(domain.domain).to eq(create_params[:domain])
expect(domain.certificate).to eq(create_params[:user_provided_certificate])
expect(domain.key).to eq(create_params[:user_provided_key])
expect(domain.wildcard).to eq(true)
expect(domain.scope).to eq('instance')
expect(domain.usage).to eq('serverless')
end
it 'redirects to index' do
post :create, params: { pages_domain: create_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('Domain was successfully created.')
end
end
context 'when there are errors' do
it 'renders index view' do
post :create, params: { pages_domain: { foo: 'bar' } }
expect(assigns(:domain).errors.size).to be > 0
expect(response).to render_template('index')
end
end
end
end
describe '#update' do
let(:domain) { create(:pages_domain, :instance_serverless) }
let(:update_params) do
sample_domain = build(:pages_domain)
{
user_provided_certificate: sample_domain.certificate,
user_provided_key: sample_domain.key
}
end
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'admin user' do
before do
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when domain exists' do
it 'updates the domain with the provided attributes' do
new_certificate = build(:pages_domain, :ecdsa).certificate
new_key = build(:pages_domain, :ecdsa).key
put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: new_certificate, user_provided_key: new_key } }
domain.reload
expect(domain.certificate).to eq(new_certificate)
expect(domain.key).to eq(new_key)
end
it 'does not update the domain name' do
put :update, params: { id: domain.id, pages_domain: { domain: 'new.com' } }
expect(domain.reload.domain).not_to eq('new.com')
end
it 'redirects to index' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('Domain was successfully updated.')
end
end
context 'when domain does not exist' do
it 'returns 404' do
put :update, params: { id: 0, pages_domain: update_params }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when there are errors' do
it 'renders index view' do
put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: 'bad certificate' } }
expect(assigns(:domain).errors.size).to be > 0
expect(response).to render_template('index')
end
end
end
end
describe '#verify' do
let(:domain) { create(:pages_domain, :instance_serverless) }
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
post :verify, params: { id: domain.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'admin user' do
before do
sign_in(admin)
end
def stub_service
service = double(:service)
expect(VerifyPagesDomainService).to receive(:new).with(domain).and_return(service)
service
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
post :verify, params: { id: domain.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
it 'handles verification success' do
expect(stub_service).to receive(:execute).and_return(status: :success)
post :verify, params: { id: domain.id }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to eq('Successfully verified domain ownership')
end
it 'handles verification failure' do
expect(stub_service).to receive(:execute).and_return(status: :failed)
post :verify, params: { id: domain.id }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:alert]).to eq('Failed to verify domain ownership')
end
end
end
describe '#destroy' do
let!(:domain) { create(:pages_domain, :instance_serverless) }
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
delete :destroy, params: { id: domain.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'admin user' do
before do
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
delete :destroy, params: { id: domain.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when domain exists' do
context 'and is not associated to any clusters' do
it 'deletes the domain' do
expect { delete :destroy, params: { id: domain.id } }
.to change { PagesDomain.count }.from(1).to(0)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include('Domain was successfully deleted.')
end
end
context 'and is associated to any clusters' do
before do
create(:serverless_domain_cluster, pages_domain: domain)
end
it 'does not delete the domain' do
expect { delete :destroy, params: { id: domain.id } }
.not_to change { PagesDomain.count }
expect(response).to have_gitlab_http_status(:conflict)
expect(flash[:notice]).to include('Domain cannot be deleted while associated to one or more clusters.')
end
end
end
context 'when domain does not exist' do
before do
domain.destroy!
end
it 'responds with 404' do
delete :destroy, params: { id: domain.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end

View File

@ -704,7 +704,7 @@ RSpec.describe ApplicationController do
get :index
expect(response.headers['Cache-Control']).to eq 'no-store'
expect(response.headers['Cache-Control']).to eq 'private, no-store'
expect(response.headers['Pragma']).to eq 'no-cache'
end
@ -740,7 +740,7 @@ RSpec.describe ApplicationController do
it 'sets no-cache headers', :aggregate_failures do
subject
expect(response.headers['Cache-Control']).to eq 'no-store'
expect(response.headers['Cache-Control']).to eq 'private, no-store'
expect(response.headers['Pragma']).to eq 'no-cache'
expect(response.headers['Expires']).to eq 'Fri, 01 Jan 1990 00:00:00 GMT'
end

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Groups::DependencyProxyForContainersController do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
include WorkhorseHelpers
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:group) { create(:group, :private) }
@ -242,16 +243,9 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
describe 'GET #blob' do
let_it_be(:blob) { create(:dependency_proxy_blob) }
let(:blob) { create(:dependency_proxy_blob, group: group) }
let(:blob_sha) { blob.file_name.sub('.gz', '') }
let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
allow(instance).to receive(:execute).and_return(blob_response)
end
end
subject { get_blob }
@ -264,40 +258,31 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
context 'remote blob request fails' do
let(:blob_response) do
{
status: :error,
http_status: 400,
message: ''
}
end
before do
group.add_guest(user)
end
it 'proxies status from the remote blob request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to be_empty
end
end
context 'a valid user' do
before do
group.add_guest(user)
end
it_behaves_like 'a successful blob pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
context 'with a cache entry' do
let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
context 'when cache entry does not exist' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
it 'returns Workhorse send-dependency instructions' do
subject
send_data_type, send_data = workhorse_send_data
header, url = send_data.values_at('Header', 'Url')
expect(send_data_type).to eq('send-dependency')
expect(header).to eq("Authorization" => ["Bearer abcd1234"])
expect(url).to eq(DependencyProxy::Registry.blob_url('alpine', blob_sha))
expect(response.headers['Content-Type']).to eq('application/gzip')
expect(response.headers['Content-Disposition']).to eq(
ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: blob.file_name)
)
end
end
end
@ -319,6 +304,74 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'a successful blob pull'
end
end
context 'when dependency_proxy_workhorse disabled' do
let(:blob_response) { { status: :success, blob: blob, from_cache: false } }
before do
stub_feature_flags(dependency_proxy_workhorse: false)
allow_next_instance_of(DependencyProxy::FindOrCreateBlobService) do |instance|
allow(instance).to receive(:execute).and_return(blob_response)
end
end
context 'remote blob request fails' do
let(:blob_response) do
{
status: :error,
http_status: 400,
message: ''
}
end
before do
group.add_guest(user)
end
it 'proxies status from the remote blob request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to be_empty
end
end
context 'a valid user' do
before do
group.add_guest(user)
end
it_behaves_like 'a successful blob pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
context 'with a cache entry' do
let(:blob_response) { { status: :success, blob: blob, from_cache: true } }
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
end
end
context 'a valid deploy token' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it_behaves_like 'a successful blob pull'
context 'pulling from a subgroup' do
let_it_be_with_reload(:parent_group) { create(:group) }
let_it_be_with_reload(:group) { create(:group, parent: parent_group) }
before do
parent_group.create_dependency_proxy_setting!(enabled: true)
group_deploy_token.update_column(:group_id, parent_group.id)
end
it_behaves_like 'a successful blob pull'
end
end
end
end
it_behaves_like 'not found when disabled'
@ -328,6 +381,61 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
describe 'GET #authorize_upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
subject(:authorize_upload_blob) do
request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
end
it_behaves_like 'without permission'
context 'with a valid user' do
before do
group.add_guest(user)
end
it 'sends Workhorse file upload instructions', :aggregate_failures do
authorize_upload_blob
expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(DependencyProxy::FileUploader.workhorse_local_upload_path)
end
end
end
describe 'GET #upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :upload_blob, params: {
group_id: group.to_param,
image: 'alpine',
sha: blob_sha,
file: file
}
end
it_behaves_like 'without permission'
context 'with a valid user' do
before do
group.add_guest(user)
expect_next_found_instance_of(Group) do |instance|
expect(instance).to receive_message_chain(:dependency_proxy_blobs, :create!)
end
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
end
end
def enable_dependency_proxy
group.create_dependency_proxy_setting!(enabled: true)
end

View File

@ -91,7 +91,7 @@ RSpec.describe Projects::DesignManagement::Designs::ResizedImageController do
# (the record that represents the design at a specific version), to
# verify that the correct file is being returned.
def etag(action)
ActionDispatch::TestResponse.new.send(:generate_weak_etag, [action.cache_key, ''])
ActionDispatch::TestResponse.new.send(:generate_weak_etag, [action.cache_key])
end
specify { expect(newest_version.sha).not_to eq(oldest_version.sha) }

View File

@ -305,7 +305,7 @@ RSpec.describe SearchController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Cache-Control']).to eq('no-store')
expect(response.headers['Cache-Control']).to eq('private, no-store')
end
end

View File

@ -52,9 +52,9 @@ FactoryBot.define do
.where(design_id: evaluator.deleted_designs.map(&:id))
.update_all(event: events[:deletion])
version.designs.reload
# Ensure version.issue == design.issue for all version.designs
version.designs.update_all(issue_id: version.issue_id)
version.designs.reload
needed = evaluator.designs_count
have = version.designs.size

View File

@ -1,89 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Admin Serverless Domains', :js do
let(:sample_domain) { build(:pages_domain) }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
it 'add domain with certificate' do
visit admin_serverless_domains_path
fill_in 'pages_domain[domain]', with: 'foo.com'
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Add domain'
expect(current_path).to eq admin_serverless_domains_path
expect(page).to have_field('pages_domain[domain]', with: 'foo.com')
expect(page).to have_field('serverless_domain_dns', with: /^\*\.foo\.com CNAME /)
expect(page).to have_field('serverless_domain_verification', with: /^_gitlab-pages-verification-code.foo.com TXT /)
expect(page).not_to have_field('pages_domain[user_provided_certificate]')
expect(page).not_to have_field('pages_domain[user_provided_key]')
expect(page).to have_content 'Unverified'
expect(page).to have_content '/CN=test-certificate'
end
it 'update domain certificate' do
visit admin_serverless_domains_path
fill_in 'pages_domain[domain]', with: 'foo.com'
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Add domain'
expect(current_path).to eq admin_serverless_domains_path
expect(page).not_to have_field('pages_domain[user_provided_certificate]')
expect(page).not_to have_field('pages_domain[user_provided_key]')
click_button 'Replace'
expect(page).to have_field('pages_domain[user_provided_certificate]')
expect(page).to have_field('pages_domain[user_provided_key]')
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Save changes'
expect(page).to have_content 'Domain was successfully updated'
expect(page).to have_content '/CN=test-certificate'
end
context 'when domain exists' do
let!(:domain) { create(:pages_domain, :instance_serverless) }
it 'displays a modal when attempting to delete a domain' do
visit admin_serverless_domains_path
click_button 'Delete domain'
page.within '#modal-delete-domain' do
expect(page).to have_content "You are about to delete #{domain.domain} from your instance."
expect(page).to have_link('Delete domain')
end
end
it 'displays a modal with disabled button if unable to delete a domain' do
create(:serverless_domain_cluster, pages_domain: domain)
visit admin_serverless_domains_path
click_button 'Delete domain'
page.within '#modal-delete-domain' do
expect(page).to have_content "You must disassociate #{domain.domain} from all clusters it is attached to before deletion."
expect(page).to have_link('Delete domain')
end
end
end
end

View File

@ -0,0 +1,108 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Group Dependency Proxy for containers', :js do
include DependencyProxyHelpers
include_context 'file upload requests helpers'
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let_it_be(:content) { fixture_file_upload("spec/fixtures/dependency_proxy/#{sha}.gz").read }
let(:image) { 'alpine' }
let(:url) { capybara_url("/v2/#{group.full_path}/dependency_proxy/containers/#{image}/blobs/sha256:#{sha}") }
let(:token) { 'token' }
let(:headers) { { 'Authorization' => "Bearer #{build_jwt(user).encoded}" } }
subject do
HTTParty.get(url, headers: headers)
end
def run_server(handler)
default_server = Capybara.server
Capybara.server = Capybara.servers[:puma]
server = Capybara::Server.new(handler)
server.boot
server
ensure
Capybara.server = default_server
end
let_it_be(:external_server) do
handler = lambda do |env|
if env['REQUEST_PATH'] == '/token'
[200, {}, [{ token: 'token' }.to_json]]
else
[200, {}, [content]]
end
end
run_server(handler)
end
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
stub_config(dependency_proxy: { enabled: true })
group.add_developer(user)
stub_const("DependencyProxy::Registry::AUTH_URL", external_server.base_url)
stub_const("DependencyProxy::Registry::LIBRARY_URL", external_server.base_url)
end
shared_examples 'responds with the file' do
it 'sends file' do
expect(subject.code).to eq(200)
expect(subject.body).to eq(content)
expect(subject.headers.to_h).to include(
"content-type" => ["application/gzip"],
"content-disposition" => ["attachment; filename=\"#{sha}.gz\"; filename*=UTF-8''#{sha}.gz"],
"content-length" => ["32"]
)
end
end
shared_examples 'caches the file' do
it 'caches the file' do
expect { subject }.to change {
group.dependency_proxy_blobs.count
}.from(0).to(1)
expect(subject.code).to eq(200)
expect(group.dependency_proxy_blobs.first.file.read).to eq(content)
end
end
context 'fetching a blob' do
context 'when the blob is cached for the group' do
let!(:dependency_proxy_blob) { create(:dependency_proxy_blob, group: group) }
it_behaves_like 'responds with the file'
context 'dependency_proxy_workhorse feature flag disabled' do
before do
stub_feature_flags({ dependency_proxy_workhorse: false })
end
it_behaves_like 'responds with the file'
end
end
end
context 'when the blob must be downloaded' do
it_behaves_like 'responds with the file'
it_behaves_like 'caches the file'
context 'dependency_proxy_workhorse feature flag disabled' do
before do
stub_feature_flags({ dependency_proxy_workhorse: false })
end
it_behaves_like 'responds with the file'
it_behaves_like 'caches the file'
end
end
end

View File

@ -68,7 +68,7 @@ RSpec.describe 'Pipeline Badge' do
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
expect(page.status_code).to eq(200)
expect(page.response_headers['Cache-Control']).to eq('no-store')
expect(page.response_headers['Cache-Control']).to eq('private, no-store')
end
end

View File

@ -64,9 +64,54 @@
"warnings": []
},
"gitaly": {
"duration": "0ms",
"calls": 0,
"details": [],
"duration": "30ms",
"calls": 2,
"details": [
{
"start": 6301.575665897,
"feature": "commit_service#get_tree_entries",
"duration": 23.709,
"request": "{:repository=>\n {:storage_name=>\"nfs-file-cny01\",\n :relative_path=>\n \"@hashed/a6/80/a68072e80f075e89bc74a300101a9e71e8363bdb542182580162553462480a52.git\",\n :git_object_directory=>\"\",\n :git_alternate_object_directories=>[],\n :gl_repository=>\"project-278964\",\n :gl_project_path=>\"gitlab-org/gitlab\"},\n :revision=>\"master\",\n :path=>\".\",\n :sort=>:TREES_FIRST,\n :pagination_params=>{:page_token=>\"\", :limit=>100}}\n",
"rpc": "get_tree_entries",
"backtrace": [
"lib/gitlab/gitaly_client/call.rb:48:in `block in instrument_stream'",
"lib/gitlab/gitaly_client/commit_service.rb:128:in `each'",
"lib/gitlab/gitaly_client/commit_service.rb:128:in `each'",
"lib/gitlab/gitaly_client/commit_service.rb:128:in `flat_map'",
"lib/gitlab/gitaly_client/commit_service.rb:128:in `tree_entries'",
"lib/gitlab/git/tree.rb:26:in `block in tree_entries'",
"lib/gitlab/git/wraps_gitaly_errors.rb:7:in `wrapped_gitaly_errors'",
"lib/gitlab/git/tree.rb:25:in `tree_entries'",
"lib/gitlab/git/rugged_impl/tree.rb:29:in `tree_entries'",
"lib/gitlab/git/tree.rb:21:in `where'",
"app/models/tree.rb:17:in `initialize'",
"app/models/repository.rb:681:in `new'",
"app/models/repository.rb:681:in `tree'",
"app/graphql/resolvers/paginated_tree_resolver.rb:35:in `resolve'",
"lib/gitlab/graphql/present/field_extension.rb:18:in `resolve'",
"lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb:7:in `resolve'",
"lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'",
"lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'",
"lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'",
"lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'",
"lib/gitlab/graphql/generic_tracing.rb:40:in `with_labkit_tracing'",
"lib/gitlab/graphql/generic_tracing.rb:30:in `platform_trace'",
"app/graphql/gitlab_schema.rb:40:in `multiplex'",
"app/controllers/graphql_controller.rb:110:in `execute_multiplex'",
"app/controllers/graphql_controller.rb:41:in `execute'",
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
"ee/app/controllers/ee/application_controller.rb:44:in `set_current_ip_address'",
"app/controllers/application_controller.rb:497:in `set_current_admin'",
"lib/gitlab/session.rb:11:in `with_session'",
"app/controllers/application_controller.rb:488:in `set_session_storage'",
"app/controllers/application_controller.rb:482:in `set_locale'",
"app/controllers/application_controller.rb:476:in `set_current_context'",
"ee/lib/omni_auth/strategies/group_saml.rb:41:in `other_phase'",
"lib/gitlab/jira/middleware.rb:19:in `call'"
],
"warnings": []
}
],
"warnings": []
},
"redis": {

View File

@ -503,6 +503,53 @@ describe('ErrorDetails', () => {
});
});
});
describe('Release links', () => {
const firstReleaseVersion = '7975be01';
const firstCommitLink = '/gitlab/-/commit/7975be01';
const firstReleaseLink = '/sentry/releases/7975be01';
const findFirstCommitLink = () => wrapper.find(`[href$="${firstCommitLink}"]`);
const findFirstReleaseLink = () => wrapper.find(`[href$="${firstReleaseLink}"]`);
const lastReleaseVersion = '6ca5a5c1';
const lastCommitLink = '/gitlab/-/commit/6ca5a5c1';
const lastReleaseLink = '/sentry/releases/6ca5a5c1';
const findLastCommitLink = () => wrapper.find(`[href$="${lastCommitLink}"]`);
const findLastReleaseLink = () => wrapper.find(`[href$="${lastReleaseLink}"]`);
it('should display links to Sentry', async () => {
mocks.$apollo.queries.error.loading = false;
await wrapper.setData({
error: {
firstReleaseVersion,
lastReleaseVersion,
externalBaseUrl: '/sentry',
},
});
expect(findFirstReleaseLink().exists()).toBe(true);
expect(findLastReleaseLink().exists()).toBe(true);
expect(findFirstCommitLink().exists()).toBe(false);
expect(findLastCommitLink().exists()).toBe(false);
});
it('should display links to GitLab when integrated', async () => {
mocks.$apollo.queries.error.loading = false;
await wrapper.setData({
error: {
firstReleaseVersion,
lastReleaseVersion,
integrated: true,
externalBaseUrl: '/gitlab',
},
});
expect(findFirstCommitLink().exists()).toBe(true);
expect(findLastCommitLink().exists()).toBe(true);
expect(findFirstReleaseLink().exists()).toBe(false);
expect(findLastReleaseLink().exists()).toBe(false);
});
});
});
describe('Snowplow tracking', () => {

View File

@ -61,11 +61,6 @@ describe('CsvExportModal', () => {
expect(wrapper.text()).toContain('10 issues selected');
expect(findIcon().exists()).toBe(true);
});
it("doesn't display the info text when issuableCount is -1", () => {
wrapper = createComponent({ props: { issuableCount: -1 } });
expect(wrapper.text()).not.toContain('issues selected');
});
});
describe('email info text', () => {

View File

@ -17,7 +17,6 @@ describe('CsvImportModal', () => {
...props,
},
provide: {
issuableType: 'issues',
...injectedProperties,
},
stubs: {
@ -43,9 +42,9 @@ describe('CsvImportModal', () => {
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
describe('template', () => {
it('displays modal title', () => {
it('passes correct title props to modal', () => {
wrapper = createComponent();
expect(findModal().text()).toContain('Import issues');
expect(findModal().props('title')).toContain('Import issues');
});
it('displays a note about the maximum allowed file size', () => {
@ -73,7 +72,7 @@ describe('CsvImportModal', () => {
});
it('submits the form when the primary action is clicked', () => {
findPrimaryButton().trigger('click');
findModal().vm.$emit('primary');
expect(formSubmitSpy).toHaveBeenCalled();
});

View File

@ -10,6 +10,7 @@ RSpec.describe GitlabSchema.types['SentryDetailedError'] do
it 'exposes the expected fields' do
expected_fields = %i[
id
integrated
sentryId
title
type

View File

@ -257,6 +257,10 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
expect(subject.gitlab_issue).to eq('https://gitlab.com/gitlab-org/gitlab/issues/1')
end
it 'has an integrated attribute set to false' do
expect(subject.integrated).to be_falsey
end
context 'when issue annotations exist' do
before do
issue_sample_response['annotations'] = [

View File

@ -16,6 +16,7 @@ RSpec.describe Gitlab::Middleware::Multipart::Handler do
::Gitlab.config.uploads.storage_path,
::JobArtifactUploader.workhorse_upload_path,
::LfsObjectUploader.workhorse_upload_path,
::DependencyProxy::FileUploader.workhorse_upload_path,
File.join(Rails.root, 'public/uploads/tmp')
]
end

View File

@ -23,11 +23,19 @@ RSpec.describe Gitlab::PerformanceBar::Stats do
expect(logger).to receive(:info)
.with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb',
method_path: 'lib/gitlab/pagination/offset_pagination.rb:add_pagination_headers',
count: 1, request_id: 'foo', type: :sql })
count: 1, request_id: 'foo', query_type: 'active-record' })
expect(logger).to receive(:info)
.with({ duration_ms: 1.634, filename: 'lib/api/helpers.rb',
method_path: 'lib/api/helpers.rb:find_project',
count: 2, request_id: 'foo', type: :sql })
count: 2, request_id: 'foo', query_type: 'active-record' })
expect(logger).to receive(:info)
.with({ duration_ms: 23.709, filename: 'lib/gitlab/gitaly_client/commit_service.rb',
method_path: 'lib/gitlab/gitaly_client/commit_service.rb:each',
count: 1, request_id: 'foo', query_type: 'gitaly' })
expect(logger).to receive(:info)
.with({ duration_ms: 0.155, filename: 'lib/feature.rb',
method_path: 'lib/feature.rb:enabled?',
count: 1, request_id: 'foo', query_type: 'redis' })
subject
end

View File

@ -11,6 +11,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
include ApplicationWorker
feature_category :issue_tracking
def self.job_for_args(args)
jobs.find { |job| job['args'] == args }
end
@ -20,8 +22,31 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
end
end
let(:not_owned_worker_class) do
Class.new(worker_class) do
def self.name
'TestNotOwnedWithContextWorker'
end
feature_category_not_owned!
end
end
let(:mailer_class) do
Class.new(ApplicationMailer) do
def self.name
'TestMailer'
end
def test_mail
end
end
end
before do
stub_const(worker_class.name, worker_class)
stub_const(not_owned_worker_class.name, not_owned_worker_class)
stub_const(mailer_class.name, mailer_class)
end
describe "#call" do
@ -41,5 +66,75 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
expect(job1['meta.user']).to eq(user_per_job['job1'].username)
expect(job2['meta.user']).to eq(user_per_job['job2'].username)
end
context 'when the feature category is set in the context_proc' do
it 'takes the feature category from the worker, not the caller' do
TestWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
job1 = TestWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestWithContextWorker.job_for_args(['job2', 1, 2, 3])
expect(job1['meta.feature_category']).to eq('issue_tracking')
expect(job2['meta.feature_category']).to eq('issue_tracking')
end
it 'takes the feature category from the caller if the worker is not owned' do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { { feature_category: 'code_review' } }
)
job1 = TestNotOwnedWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestNotOwnedWithContextWorker.job_for_args(['job2', 1, 2, 3])
expect(job1['meta.feature_category']).to eq('code_review')
expect(job2['meta.feature_category']).to eq('code_review')
end
it 'does not set any explicit feature category for mailers', :sidekiq_mailers do
expect(Gitlab::ApplicationContext).not_to receive(:with_context)
TestMailer.test_mail.deliver_later
end
end
context 'when the feature category is already set in the surrounding block' do
it 'takes the feature category from the worker, not the caller' do
Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
TestWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
end
job1 = TestWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestWithContextWorker.job_for_args(['job2', 1, 2, 3])
expect(job1['meta.feature_category']).to eq('issue_tracking')
expect(job2['meta.feature_category']).to eq('issue_tracking')
end
it 'takes the feature category from the caller if the worker is not owned' do
Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
context_proc: -> (_) { {} }
)
end
job1 = TestNotOwnedWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestNotOwnedWithContextWorker.job_for_args(['job2', 1, 2, 3])
expect(job1['meta.feature_category']).to eq('authentication_and_authorization')
expect(job2['meta.feature_category']).to eq('authentication_and_authorization')
end
end
end
end

View File

@ -5,23 +5,88 @@ require 'spec_helper'
RSpec.describe ::Gitlab::SubscriptionPortal do
using RSpec::Parameterized::TableSyntax
where(:method_name, :test, :development, :result) do
:default_subscriptions_url | false | false | 'https://customers.gitlab.com'
:default_subscriptions_url | false | true | 'https://customers.stg.gitlab.com'
:default_subscriptions_url | true | false | 'https://customers.stg.gitlab.com'
:payment_form_url | false | false | 'https://customers.gitlab.com/payment_forms/cc_validation'
:payment_form_url | false | true | 'https://customers.stg.gitlab.com/payment_forms/cc_validation'
:payment_form_url | true | false | 'https://customers.stg.gitlab.com/payment_forms/cc_validation'
let(:env_value) { nil }
before do
stub_env('CUSTOMER_PORTAL_URL', env_value)
end
with_them do
subject { described_class.method(method_name).call }
describe '.default_subscriptions_url' do
where(:test, :development, :result) do
false | false | 'https://customers.gitlab.com'
false | true | 'https://customers.stg.gitlab.com'
true | false | 'https://customers.stg.gitlab.com'
end
before do
allow(Rails).to receive_message_chain(:env, :test?).and_return(test)
allow(Rails).to receive_message_chain(:env, :development?).and_return(development)
end
it { is_expected.to eq(result) }
with_them do
subject { described_class.default_subscriptions_url }
it { is_expected.to eq(result) }
end
end
describe '.subscriptions_url' do
subject { described_class.subscriptions_url }
context 'when CUSTOMER_PORTAL_URL ENV is unset' do
it { is_expected.to eq('https://customers.stg.gitlab.com') }
end
context 'when CUSTOMER_PORTAL_URL ENV is set' do
let(:env_value) { 'https://customers.example.com' }
it { is_expected.to eq(env_value) }
end
end
context 'url methods' do
where(:method_name, :result) do
:default_subscriptions_url | 'https://customers.stg.gitlab.com'
:payment_form_url | 'https://customers.stg.gitlab.com/payment_forms/cc_validation'
:subscriptions_graphql_url | 'https://customers.stg.gitlab.com/graphql'
:subscriptions_more_minutes_url | 'https://customers.stg.gitlab.com/buy_pipeline_minutes'
:subscriptions_more_storage_url | 'https://customers.stg.gitlab.com/buy_storage'
:subscriptions_manage_url | 'https://customers.stg.gitlab.com/subscriptions'
:subscriptions_plans_url | 'https://customers.stg.gitlab.com/plans'
:subscriptions_instance_review_url | 'https://customers.stg.gitlab.com/instance_review'
:subscriptions_gitlab_plans_url | 'https://customers.stg.gitlab.com/gitlab_plans'
:subscriptions_comparison_url | 'https://about.gitlab.com/pricing/gitlab-com/feature-comparison'
end
with_them do
subject { described_class.send(method_name) }
it { is_expected.to eq(result) }
end
end
describe '.add_extra_seats_url' do
subject { described_class.add_extra_seats_url(group_id) }
let(:group_id) { 153 }
it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/extra_seats") }
end
describe '.upgrade_subscription_url' do
subject { described_class.upgrade_subscription_url(group_id, plan_id) }
let(:group_id) { 153 }
let(:plan_id) { 5 }
it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}") }
end
describe '.renew_subscription_url' do
subject { described_class.renew_subscription_url(group_id) }
let(:group_id) { 153 }
it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/renew") }
end
end

View File

@ -81,6 +81,13 @@ RSpec.describe ErrorTracking::Error, type: :model do
end
describe '#to_sentry_detailed_error' do
it { expect(error.to_sentry_detailed_error).to be_kind_of(Gitlab::ErrorTracking::DetailedError) }
let_it_be(:event) { create(:error_tracking_error_event, error: error) }
subject { error.to_sentry_detailed_error }
it { is_expected.to be_kind_of(Gitlab::ErrorTracking::DetailedError) }
it { expect(subject.integrated).to be_truthy }
it { expect(subject.first_release_version).to eq('db853d7') }
it { expect(subject.last_release_version).to eq('db853d7') }
end
end

View File

@ -2756,6 +2756,10 @@ RSpec.describe Group do
it 'removes the protocol' do
expect(group.dependency_proxy_image_prefix).not_to include('http')
end
it 'does not include /groups' do
expect(group.dependency_proxy_image_prefix).not_to include('/groups')
end
end
describe '#dependency_proxy_image_ttl_policy' do

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Namespace::TraversalHierarchy, type: :model do
let_it_be(:root, reload: true) { create(:group, :with_hierarchy) }
let!(:root) { create(:group, :with_hierarchy) }
describe '.for_namespace' do
let(:hierarchy) { described_class.for_namespace(group) }
@ -62,7 +62,12 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do
it { expect(hierarchy.incorrect_traversal_ids).to be_empty }
it_behaves_like 'hierarchy with traversal_ids'
it_behaves_like 'hierarchy with traversal_ids' do
before do
subject
end
end
it_behaves_like 'locked row' do
let(:recorded_queries) { ActiveRecord::QueryRecorder.new }
let(:row) { root }

View File

@ -495,23 +495,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#merge_requests_author_approval' do
where(:attribute_value, :return_value) do
true | true
false | false
nil | false
end
with_them do
let(:project) { create(:project, merge_requests_author_approval: attribute_value) }
it 'returns expected value' do
expect(project.merge_requests_author_approval).to eq(return_value)
expect(project.merge_requests_author_approval?).to eq(return_value)
end
end
end
describe '#all_pipelines' do
let_it_be(:project) { create(:project) }

View File

@ -728,16 +728,16 @@ RSpec.describe API::Groups do
end
it 'avoids N+1 queries with project links' do
get api("/groups/#{group1.id}", admin)
get api("/groups/#{group1.id}", user1)
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}", admin)
get api("/groups/#{group1.id}", user1)
end.count
create(:project, namespace: group1)
expect do
get api("/groups/#{group1.id}", admin)
get api("/groups/#{group1.id}", user1)
end.not_to exceed_query_limit(control_count)
end
@ -746,7 +746,7 @@ RSpec.describe API::Groups do
create(:group_group_link, shared_group: group1, shared_with_group: create(:group))
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}", admin)
get api("/groups/#{group1.id}", user1)
end.count
# setup "n" more shared groups
@ -755,7 +755,7 @@ RSpec.describe API::Groups do
# test that no of queries for 1 shared group is same as for n shared groups
expect do
get api("/groups/#{group1.id}", admin)
get api("/groups/#{group1.id}", user1)
end.not_to exceed_query_limit(control_count)
end
end
@ -1179,6 +1179,20 @@ RSpec.describe API::Groups do
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project1.name)
end
it 'avoids N+1 queries' do
get api("/groups/#{group1.id}/projects", user1)
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}/projects", user1)
end.count
create(:project, namespace: group1)
expect do
get api("/groups/#{group1.id}/projects", user1)
end.not_to exceed_query_limit(control_count)
end
end
context "when authenticated as admin" do
@ -1196,20 +1210,6 @@ RSpec.describe API::Groups do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'avoids N+1 queries' do
get api("/groups/#{group1.id}/projects", admin)
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}/projects", admin)
end.count
create(:project, namespace: group1)
expect do
get api("/groups/#{group1.id}/projects", admin)
end.not_to exceed_query_limit(control_count)
end
end
context 'when using group path in URL' do

View File

@ -1457,10 +1457,20 @@ RSpec.describe API::Users do
describe "PUT /user/:id/credit_card_validation" do
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
let(:expiration_year) { Date.today.year + 10 }
let(:params) do
{
credit_card_validated_at: credit_card_validated_time,
credit_card_expiration_year: expiration_year,
credit_card_expiration_month: 1,
credit_card_holder_name: 'John Smith',
credit_card_mask_number: '1111'
}
end
context 'when unauthenticated' do
it 'returns authentication error' do
put api("/user/#{user.id}/credit_card_validation"), params: { credit_card_validated_at: credit_card_validated_time }
put api("/user/#{user.id}/credit_card_validation"), params: {}
expect(response).to have_gitlab_http_status(:unauthorized)
end
@ -1468,7 +1478,7 @@ RSpec.describe API::Users do
context 'when authenticated as non-admin' do
it "does not allow updating user's credit card validation", :aggregate_failures do
put api("/user/#{user.id}/credit_card_validation", user), params: { credit_card_validated_at: credit_card_validated_time }
put api("/user/#{user.id}/credit_card_validation", user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@ -1476,10 +1486,17 @@ RSpec.describe API::Users do
context 'when authenticated as admin' do
it "updates user's credit card validation", :aggregate_failures do
put api("/user/#{user.id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
put api("/user/#{user.id}/credit_card_validation", admin), params: params
user.reload
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
expect(user.credit_card_validation).to have_attributes(
credit_card_validated_at: credit_card_validated_time,
expiration_date: Date.new(expiration_year, 1, 31),
last_digits: 1111,
holder_name: 'John Smith'
)
end
it "returns 400 error if credit_card_validated_at is missing" do
@ -1489,7 +1506,7 @@ RSpec.describe API::Users do
end
it 'returns 404 error if user not found' do
put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::Serverless::DomainsController do
it 'routes to #index' do
expect(get: '/admin/serverless/domains').to route_to('admin/serverless/domains#index')
end
it 'routes to #create' do
expect(post: '/admin/serverless/domains/').to route_to('admin/serverless/domains#create')
end
it 'routes to #update' do
expect(put: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1')
expect(patch: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1')
end
it 'routes #verify' do
expect(post: '/admin/serverless/domains/1/verify').to route_to(controller: 'admin/serverless/domains', action: 'verify', id: '1')
end
end

View File

@ -173,14 +173,6 @@ RSpec.describe Projects::UpdatePagesService do
include_examples 'successfully deploys'
context 'when pages_smart_check_outdated_sha feature flag is disabled' do
before do
stub_feature_flags(pages_smart_check_outdated_sha: false)
end
include_examples 'fails with outdated reference message'
end
context 'when old deployment present' do
before do
old_build = create(:ci_build, pipeline: old_pipeline, ref: 'HEAD')
@ -189,14 +181,6 @@ RSpec.describe Projects::UpdatePagesService do
end
include_examples 'successfully deploys'
context 'when pages_smart_check_outdated_sha feature flag is disabled' do
before do
stub_feature_flags(pages_smart_check_outdated_sha: false)
end
include_examples 'fails with outdated reference message'
end
end
context 'when newer deployment present' do

View File

@ -7,7 +7,17 @@ RSpec.describe Users::UpsertCreditCardValidationService do
let(:user_id) { user.id }
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
let(:params) { { user_id: user_id, credit_card_validated_at: credit_card_validated_time } }
let(:expiration_year) { Date.today.year + 10 }
let(:params) do
{
user_id: user_id,
credit_card_validated_at: credit_card_validated_time,
credit_card_expiration_year: expiration_year,
credit_card_expiration_month: 1,
credit_card_holder_name: 'John Smith',
credit_card_mask_number: '1111'
}
end
describe '#execute' do
subject(:service) { described_class.new(params) }
@ -52,6 +62,16 @@ RSpec.describe Users::UpsertCreditCardValidationService do
end
end
shared_examples 'returns an error, tracking the exception' do
it do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
result = service.execute
expect(result.status).to eq(:error)
end
end
context 'when user id does not exist' do
let(:user_id) { non_existing_record_id }
@ -61,19 +81,27 @@ RSpec.describe Users::UpsertCreditCardValidationService do
context 'when missing credit_card_validated_at' do
let(:params) { { user_id: user_id } }
it_behaves_like 'returns an error without tracking the exception'
it_behaves_like 'returns an error, tracking the exception'
end
context 'when missing user id' do
let(:params) { { credit_card_validated_at: credit_card_validated_time } }
it_behaves_like 'returns an error without tracking the exception'
it_behaves_like 'returns an error, tracking the exception'
end
context 'when unexpected exception happen' do
it 'tracks the exception and returns an error' do
logged_params = {
credit_card_validated_at: credit_card_validated_time,
expiration_date: Date.new(expiration_year, 1, 31),
holder_name: "John Smith",
last_digits: 1111,
user_id: user_id
}
expect(::Users::CreditCardValidation).to receive(:upsert).and_raise(e = StandardError.new('My exception!'))
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, class: described_class.to_s, params: params)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, class: described_class.to_s, params: logged_params)
result = service.execute

View File

@ -3,14 +3,30 @@
RSpec::Matchers.define_negated_matcher :be_nullable, :be_non_null
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
match do |klass|
permissions = if klass.respond_to?(:required_permissions)
klass.required_permissions
else
[klass.to_graphql.metadata[:authorize]]
end
def permissions_for(klass)
if klass.respond_to?(:required_permissions)
klass.required_permissions
else
[klass.to_graphql.metadata[:authorize]]
end
end
expect(permissions).to eq(expected)
match do |klass|
actual = permissions_for(klass)
expect(actual).to match_array(expected)
end
failure_message do |klass|
actual = permissions_for(klass)
missing = actual - expected
extra = expected - actual
message = []
message << "is missing permissions: #{missing.inspect}" if missing.any?
message << "contained unexpected permissions: #{extra.inspect}" if extra.any?
message.join("\n")
end
end

View File

@ -299,7 +299,7 @@ RSpec.shared_examples 'wiki controller actions' do
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true')
expect(response.cache_control[:public]).to be(false)
expect(response.headers['Cache-Control']).to eq('no-store')
expect(response.headers['Cache-Control']).to eq('private, no-store')
end
end
end

View File

@ -155,8 +155,6 @@ type Response struct {
ProcessLsifReferences bool
// The maximum accepted size in bytes of the upload
MaximumSize int64
// DEPRECATED: Feature flag used to determine whether to strip the multipart filename of any directories
FeatureFlagExtractBase bool
}
// singleJoiningSlash is taken from reverseproxy.go:singleJoiningSlash

View File

@ -0,0 +1,125 @@
package dependencyproxy
import (
"context"
"fmt"
"io"
"net"
"net/http"
"time"
"gitlab.com/gitlab-org/labkit/correlation"
"gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/labkit/tracing"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
)
// httpTransport defines a http.Transport with values
// that are more restrictive than for http.DefaultTransport,
// they define shorter TLS Handshake, and more aggressive connection closing
// to prevent the connection hanging and reduce FD usage
var httpTransport = tracing.NewRoundTripper(correlation.NewInstrumentedRoundTripper(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 10 * time.Second,
}).DialContext,
MaxIdleConns: 2,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
}))
var httpClient = &http.Client{
Transport: httpTransport,
}
type Injector struct {
senddata.Prefix
uploadHandler http.Handler
}
type entryParams struct {
Url string
Header http.Header
}
type nullResponseWriter struct {
header http.Header
status int
}
func (nullResponseWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (w *nullResponseWriter) Header() http.Header {
return w.header
}
func (w *nullResponseWriter) WriteHeader(status int) {
if w.status == 0 {
w.status = status
}
}
func NewInjector() *Injector {
return &Injector{
Prefix: "send-dependency:",
}
}
func (p *Injector) SetUploadHandler(uploadHandler http.Handler) {
p.uploadHandler = uploadHandler
}
func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
dependencyResponse, err := p.fetchUrl(r.Context(), sendData)
if err != nil {
helper.Fail500(w, r, err)
return
}
defer dependencyResponse.Body.Close()
if dependencyResponse.StatusCode >= 400 {
w.WriteHeader(dependencyResponse.StatusCode)
io.Copy(w, dependencyResponse.Body)
return
}
teeReader := io.TeeReader(dependencyResponse.Body, w)
saveFileRequest, err := http.NewRequestWithContext(r.Context(), "POST", r.URL.String()+"/upload", teeReader)
if err != nil {
helper.Fail500(w, r, fmt.Errorf("dependency proxy: failed to create request: %w", err))
}
saveFileRequest.Header = helper.HeaderClone(r.Header)
saveFileRequest.ContentLength = dependencyResponse.ContentLength
w.Header().Del("Content-Length")
nrw := &nullResponseWriter{header: http.Header{}}
p.uploadHandler.ServeHTTP(nrw, saveFileRequest)
if nrw.status != http.StatusOK {
fields := log.Fields{"code": nrw.status}
helper.Fail500WithFields(nrw, r, fmt.Errorf("dependency proxy: failed to upload file"), fields)
}
}
func (p *Injector) fetchUrl(ctx context.Context, sendData string) (*http.Response, error) {
var params entryParams
if err := p.Unpack(&params, sendData); err != nil {
return nil, fmt.Errorf("dependency proxy: unpack sendData: %v", err)
}
r, err := http.NewRequestWithContext(ctx, "GET", params.Url, nil)
if err != nil {
return nil, fmt.Errorf("dependency proxy: failed to fetch dependency: %v", err)
}
r.Header = params.Header
return httpClient.Do(r)
}

View File

@ -0,0 +1,98 @@
package dependencyproxy
import (
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/stretchr/testify/require"
)
type fakeUploadHandler struct {
request *http.Request
body []byte
handler func(w http.ResponseWriter, r *http.Request)
}
func (f *fakeUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.request = r
f.body, _ = io.ReadAll(r.Body)
f.handler(w, r)
}
func TestSuccessfullRequest(t *testing.T) {
content := []byte("result")
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
w.Write(content)
}))
uploadHandler := &fakeUploadHandler{
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
},
}
injector := NewInjector()
injector.SetUploadHandler(uploadHandler)
response := makeRequest(injector, `{"Token": "token", "Url": "`+originResourceServer.URL+`/url"}`)
require.Equal(t, "/target/upload", uploadHandler.request.URL.Path)
require.Equal(t, int64(6), uploadHandler.request.ContentLength)
require.Equal(t, content, uploadHandler.body)
require.Equal(t, 200, response.Code)
require.Equal(t, string(content), response.Body.String())
}
func TestIncorrectSendData(t *testing.T) {
response := makeRequest(NewInjector(), "")
require.Equal(t, 500, response.Code)
require.Equal(t, "Internal server error\n", response.Body.String())
}
func TestIncorrectSendDataUrl(t *testing.T) {
response := makeRequest(NewInjector(), `{"Token": "token", "Url": "url"}`)
require.Equal(t, 500, response.Code)
require.Equal(t, "Internal server error\n", response.Body.String())
}
func TestFailedOriginServer(t *testing.T) {
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("Not found"))
}))
uploadHandler := &fakeUploadHandler{
handler: func(w http.ResponseWriter, r *http.Request) {
require.FailNow(t, "the error response must not be uploaded")
},
}
injector := NewInjector()
injector.SetUploadHandler(uploadHandler)
response := makeRequest(injector, `{"Token": "token", "Url": "`+originResourceServer.URL+`/url"}`)
require.Equal(t, 404, response.Code)
require.Equal(t, "Not found", response.Body.String())
}
func makeRequest(injector *Injector, data string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/target", nil)
sendData := base64.StdEncoding.EncodeToString([]byte(data))
injector.Inject(w, r, sendData)
return w
}

View File

@ -16,6 +16,7 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/builds"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/channel"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/dependencyproxy"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/git"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/imageresizer"
@ -170,7 +171,7 @@ func (ro *routeEntry) isMatch(cleanedPath string, req *http.Request) bool {
return ok
}
func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg config.Config) http.Handler {
func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg config.Config, dependencyProxyInjector *dependencyproxy.Injector) http.Handler {
proxier := proxypkg.NewProxy(backend, version, rt)
return senddata.SendData(
@ -183,6 +184,7 @@ func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg conf
artifacts.SendEntry,
sendurl.SendURL,
imageresizer.NewResizer(cfg),
dependencyProxyInjector,
)
}
@ -193,7 +195,8 @@ func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg conf
func configureRoutes(u *upstream) {
api := u.APIClient
static := &staticpages.Static{DocumentRoot: u.DocumentRoot, Exclude: staticExclude}
proxy := buildProxy(u.Backend, u.Version, u.RoundTripper, u.Config)
dependencyProxyInjector := dependencyproxy.NewInjector()
proxy := buildProxy(u.Backend, u.Version, u.RoundTripper, u.Config, dependencyProxyInjector)
cableProxy := proxypkg.NewProxy(u.CableBackend, u.Version, u.CableRoundTripper)
assetsNotFoundHandler := NotFoundUnless(u.DevelopmentMode, proxy)
@ -207,7 +210,7 @@ func configureRoutes(u *upstream) {
}
signingTripper := secret.NewRoundTripper(u.RoundTripper, u.Version)
signingProxy := buildProxy(u.Backend, u.Version, signingTripper, u.Config)
signingProxy := buildProxy(u.Backend, u.Version, signingTripper, u.Config, dependencyProxyInjector)
preparers := createUploadPreparers(u.Config)
uploadPath := path.Join(u.DocumentRoot, "uploads/tmp")
@ -215,6 +218,8 @@ func configureRoutes(u *upstream) {
ciAPIProxyQueue := queueing.QueueRequests("ci_api_job_requests", uploadAccelerateProxy, u.APILimit, u.APIQueueLimit, u.APIQueueTimeout)
ciAPILongPolling := builds.RegisterHandler(ciAPIProxyQueue, redis.WatchKey, u.APICILongPollingDuration)
dependencyProxyInjector.SetUploadHandler(upload.BodyUploader(api, signingProxy, preparers.packages))
// Serve static files or forward the requests
defaultUpstream := static.ServeExisting(
u.URLPrefix,

View File

@ -934,3 +934,101 @@ func TestHealthChecksUnreachable(t *testing.T) {
})
}
}
func TestDependencyProxyInjector(t *testing.T) {
token := "token"
bodyLength := 4096 * 12
expectedBody := strings.Repeat("p", bodyLength)
testCases := []struct {
desc string
contentLength int
readSize int
finalizeHandler func(*testing.T, http.ResponseWriter)
}{
{
desc: "the uploading successfully finalized",
contentLength: bodyLength,
readSize: bodyLength,
finalizeHandler: func(t *testing.T, w http.ResponseWriter) {
w.WriteHeader(200)
},
}, {
desc: "the uploading failed",
contentLength: bodyLength,
readSize: bodyLength,
finalizeHandler: func(t *testing.T, w http.ResponseWriter) {
w.WriteHeader(500)
},
}, {
desc: "the origin resource server returns partial response",
contentLength: bodyLength + 1000,
readSize: bodyLength,
finalizeHandler: func(t *testing.T, _ http.ResponseWriter) {
t.Fatal("partial file must not be saved")
},
}, {
desc: "a user does not read the whole file",
contentLength: bodyLength,
readSize: bodyLength - 1000,
finalizeHandler: func(t *testing.T, _ http.ResponseWriter) {
t.Fatal("partial file must not be saved")
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
originResource := "/origin_resource"
originResourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, originResource, r.URL.String())
w.Header().Set("Content-Length", strconv.Itoa(tc.contentLength))
_, err := io.WriteString(w, expectedBody)
require.NoError(t, err)
}))
defer originResourceServer.Close()
originResourceUrl := originResourceServer.URL + originResource
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
switch r.URL.String() {
case "/base":
params := `{"Url": "` + originResourceUrl + `", "Token": "` + token + `"}`
w.Header().Set("Gitlab-Workhorse-Send-Data", `send-dependency:`+base64.URLEncoding.EncodeToString([]byte(params)))
case "/base/upload/authorize":
w.Header().Set("Content-Type", api.ResponseContentType)
_, err := fmt.Fprintf(w, `{"TempPath":"%s"}`, scratchDir)
require.NoError(t, err)
case "/base/upload":
tc.finalizeHandler(t, w)
default:
t.Fatalf("unexpected request: %s", r.URL)
}
})
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
req, err := http.NewRequest("GET", ws.URL+"/base", nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body := make([]byte, tc.readSize)
_, err = io.ReadFull(resp.Body, body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close()) // Client closes connection
ws.Close() // Wait for server handler to return
require.Equal(t, 200, resp.StatusCode, "status code")
require.Equal(t, expectedBody[0:tc.readSize], string(body), "response body")
})
}
}

View File

@ -1189,15 +1189,15 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@rails/actioncable@6.1.3-2":
version "6.1.3-2"
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.3-2.tgz#de22e2d7474dcca051f7060829450412a17ecc04"
integrity sha512-3mBLDwM85oj0Ot+wgC3c0wsfx5qvf8XJwSbkJk4ZqW4bA7ctn8BFW+cRQxrnQau+NDfmJvSECY8mmNIANcpULA==
"@rails/actioncable@6.1.4-1":
version "6.1.4-1"
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.4-1.tgz#69982e7f352d732f71fda0cc01b7ba8269c9945b"
integrity sha512-b6sLoMop3gX22Wm2P5LPpKcZGwsf1ZoAGS+g1HrTrdlsZ/ENOKIBiSNnHOJajHwcYlF0TefBs7e7jIYZHVYihQ==
"@rails/ujs@6.1.3-2":
version "6.1.3-2"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.3-2.tgz#5d7e161e7061654e738a116a7ec8b58b51721a11"
integrity sha512-Nd0Im4cW8tIX8ZR3jE/dS3wnJrN46RJSdCfU59Cji2puctIWohq63LjKFMufUwm21bCasISNGoLdkr3S7nwONw==
"@rails/ujs@6.1.4-1":
version "6.1.4-1"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.4-1.tgz#37507fe288a1c7c3a593602aa4dea42e5cb5797f"
integrity sha512-Fewm2wHk1n6Kf4E86dzzHDJOFg4EWcSHH3FsMEGs59bTdmf7099mjkOssOQtBqju4R39iaAOQNui7r8P+Q5Dgg==
"@sentry/browser@5.30.0":
version "5.30.0"