Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c9b0dfef1b
commit
a84626f13d
2
Gemfile
2
Gemfile
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
122
Gemfile.lock
122
Gemfile.lock
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
|
|||
gitlabCommit
|
||||
gitlabCommitPath
|
||||
gitlabIssuePath
|
||||
integrated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
def release
|
||||
payload.dig('release')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_stacktrace
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DependencyProxy::FileUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
||||
before :cache, :set_content_type
|
||||
|
|
|
|||
|
|
@ -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" } ×
|
||||
|
||||
.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
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ module Gitlab
|
|||
:gitlab_issue,
|
||||
:gitlab_project,
|
||||
:id,
|
||||
:integrated,
|
||||
:last_release_last_commit,
|
||||
:last_release_short_version,
|
||||
:last_release_version,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Sidebars
|
|||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'environment'
|
||||
'deployments'
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe GitlabSchema.types['SentryDetailedError'] do
|
|||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
id
|
||||
integrated
|
||||
sentryId
|
||||
title
|
||||
type
|
||||
|
|
|
|||
|
|
@ -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'] = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(¶ms, 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
yarn.lock
16
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue