Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-05 18:12:53 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-05 18:12:53 +0300
commita84626f13d61d190b2db5e44caf71b22fc541276 (patch)
tree5cf591ce134ac0ad5b8c101e3518b2e49101b6ad
parentc9b0dfef1ba43a9e04264023b08c589bcb9eb397 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock122
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue22
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql1
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue69
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_export_buttons.vue9
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_modal.vue68
-rw-r--r--app/assets/javascripts/pages/admin/serverless/domains/index.js17
-rw-r--r--app/assets/stylesheets/pages/pages.scss12
-rw-r--r--app/controllers/admin/instance_review_controller.rb2
-rw-r--r--app/controllers/admin/serverless/domains_controller.rb78
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb46
-rw-r--r--app/graphql/types/error_tracking/sentry_detailed_error_type.rb3
-rw-r--r--app/helpers/workhorse_helper.rb9
-rw-r--r--app/models/error_tracking/error.rb15
-rw-r--r--app/models/error_tracking/error_event.rb4
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb4
-rw-r--r--app/services/projects/update_pages_service.rb10
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb17
-rw-r--r--app/uploaders/dependency_proxy/file_uploader.rb1
-rw-r--r--app/views/admin/serverless/domains/_form.html.haml99
-rw-r--r--app/views/admin/serverless/domains/index.html.haml25
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml5
-rw-r--r--config/feature_flags/development/dependency_proxy_workhorse.yml (renamed from config/feature_flags/development/pages_smart_check_outdated_sha.yml)10
-rw-r--r--config/feature_flags/development/serverless_domain.yml8
-rw-r--r--config/feature_flags/development/show_author_on_note.yml8
-rw-r--r--config/initializers/postgresql_cte.rb2
-rw-r--r--config/routes/admin.rb8
-rw-r--r--config/routes/group.rb2
-rw-r--r--doc/administration/logs.md2
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/development/documentation/styleguide/index.md57
-rw-r--r--doc/user/project/merge_requests/approvals/settings.md20
-rw-r--r--lib/api/entities/group_detail.rb4
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/projects_relation_builder.rb2
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/error_tracking/sentry_client/issue.rb3
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb1
-rw-r--r--lib/gitlab/middleware/multipart.rb1
-rw-r--r--lib/gitlab/performance_bar/stats.rb18
-rw-r--r--lib/gitlab/sidekiq_config/dummy_worker.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/client.rb14
-rw-r--r--lib/gitlab/subscription_portal.rb20
-rw-r--r--lib/gitlab/workhorse.rb12
-rw-r--r--lib/sidebars/projects/menus/deployments_menu.rb2
-rw-r--r--locale/gitlab.pot57
-rw-r--r--package.json4
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--spec/controllers/admin/instance_review_controller_spec.rb6
-rw-r--r--spec/controllers/admin/serverless/domains_controller_spec.rb370
-rw-r--r--spec/controllers/application_controller_spec.rb4
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb176
-rw-r--r--spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb2
-rw-r--r--spec/controllers/search_controller_spec.rb2
-rw-r--r--spec/factories/design_management/versions.rb2
-rw-r--r--spec/features/admin/admin_serverless_domains_spec.rb89
-rw-r--r--spec/features/groups/dependency_proxy_for_containers_spec.rb108
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/fixtures/lib/gitlab/performance_bar/peek_data.json51
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js47
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js5
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js7
-rw-r--r--spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb1
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/multipart/handler_spec.rb1
-rw-r--r--spec/lib/gitlab/performance_bar/stats_spec.rb12
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb95
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb85
-rw-r--r--spec/models/error_tracking/error_spec.rb9
-rw-r--r--spec/models/group_spec.rb4
-rw-r--r--spec/models/namespace/traversal_hierarchy_spec.rb9
-rw-r--r--spec/models/project_spec.rb17
-rw-r--r--spec/requests/api/groups_spec.rb38
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--spec/routing/admin/serverless/domains_controller_routing_spec.rb22
-rw-r--r--spec/services/projects/update_pages_service_spec.rb16
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb36
-rw-r--r--spec/support/matchers/graphql_matchers.rb28
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--workhorse/internal/api/api.go2
-rw-r--r--workhorse/internal/dependencyproxy/dependencyproxy.go125
-rw-r--r--workhorse/internal/dependencyproxy/dependencyproxy_test.go98
-rw-r--r--workhorse/internal/upstream/routes.go11
-rw-r--r--workhorse/main_test.go98
-rw-r--r--yarn.lock18
90 files changed, 1386 insertions, 1161 deletions
diff --git a/Gemfile b/Gemfile
index 83191f273f9..45dc4dfebed 100644
--- a/Gemfile
+++ b/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'
diff --git a/Gemfile.lock b/Gemfile.lock
index d9d01ee6bd3..fc13e8d6ecc 100644
--- a/Gemfile.lock
+++ b/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)
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 0a15cb56447..4adbf5362b7 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -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>
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
index 593cbf2ae52..af386528f00 100644
--- a/app/assets/javascripts/error_tracking/queries/details.query.graphql
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -23,6 +23,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
gitlabCommit
gitlabCommitPath
gitlabIssuePath
+ integrated
}
}
}
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index 1c88f8dfdca..b0af3612e05 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -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>
diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
index 4fdd094072c..269f720bac9 100644
--- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
@@ -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
diff --git a/app/assets/javascripts/issuable/components/csv_import_modal.vue b/app/assets/javascripts/issuable/components/csv_import_modal.vue
index c85efd60b8b..b72abe14ee1 100644
--- a/app/assets/javascripts/issuable/components/csv_import_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_modal.vue
@@ -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>
diff --git a/app/assets/javascripts/pages/admin/serverless/domains/index.js b/app/assets/javascripts/pages/admin/serverless/domains/index.js
deleted file mode 100644
index 4fab7a1d9cb..00000000000
--- a/app/assets/javascripts/pages/admin/serverless/domains/index.js
+++ /dev/null
@@ -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');
- });
-}
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
index 93caa345f8a..ebaf875ad8f 100644
--- a/app/assets/stylesheets/pages/pages.scss
+++ b/app/assets/stylesheets/pages/pages.scss
@@ -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;
}
diff --git a/app/controllers/admin/instance_review_controller.rb b/app/controllers/admin/instance_review_controller.rb
index 88ca2c88aab..5567ffbdc84 100644
--- a/app/controllers/admin/instance_review_controller.rb
+++ b/app/controllers/admin/instance_review_controller.rb
@@ -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
diff --git a/app/controllers/admin/serverless/domains_controller.rb b/app/controllers/admin/serverless/domains_controller.rb
deleted file mode 100644
index 99eea8c35b4..00000000000
--- a/app/controllers/admin/serverless/domains_controller.rb
+++ /dev/null
@@ -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
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index f7dc552bd3e..e19b8ae35f8 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -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
diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
index 79e789d3f8b..826ae61a1a3 100644
--- a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
@@ -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,
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 8785c4cdcbb..4862282bc73 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -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
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 39ecc487806..2d6a4694def 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -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
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 4de13de7e2e..686518a39fb 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -22,6 +22,10 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
)
end
+ def release
+ payload.dig('release')
+ end
+
private
def build_stacktrace
diff --git a/app/models/group.rb b/app/models/group.rb
index 23b0d7e2197..77cdff68d49 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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
diff --git a/app/models/note.rb b/app/models/note.rb
index 2defa1d1ca5..37473518892 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 39ddec223e7..7ecd736b410 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index dc75fe1014a..0000e713cb4 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -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
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 70a96b3ec6b..86b5b923418 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -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
diff --git a/app/uploaders/dependency_proxy/file_uploader.rb b/app/uploaders/dependency_proxy/file_uploader.rb
index 5154f180454..f0222d4cf06 100644
--- a/app/uploaders/dependency_proxy/file_uploader.rb
+++ b/app/uploaders/dependency_proxy/file_uploader.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class DependencyProxy::FileUploader < GitlabUploader
+ extend Workhorse::UploadPath
include ObjectStorage::Concern
before :cache, :set_content_type
diff --git a/app/views/admin/serverless/domains/_form.html.haml b/app/views/admin/serverless/domains/_form.html.haml
deleted file mode 100644
index a3e1ccc5d4a..00000000000
--- a/app/views/admin/serverless/domains/_form.html.haml
+++ /dev/null
@@ -1,99 +0,0 @@
-- form_name = 'js-serverless-domain-settings'
-- form_url = @domain.persisted? ? admin_serverless_domain_path(@domain.id, anchor: form_name) : admin_serverless_domains_path(anchor: form_name)
-- show_certificate_card = @domain.persisted? && @domain.errors.blank?
-= form_for @domain, url: form_url, html: { class: 'fieldset-form' } do |f|
- = form_errors(@domain)
-
- %fieldset
- - if @domain.persisted?
- - dns_record = "*.#{@domain.domain} CNAME #{Settings.pages.host}."
- - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
- .form-group.row
- .col-sm-6.position-relative
- = f.label :domain, _('Domain'), class: 'label-bold'
- = f.text_field :domain, class: 'form-control has-floating-status-badge', readonly: true
- .status-badge.floating-status-badge
- - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
- .badge{ class: status }
- = text
- = link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "gl-button btn has-tooltip", title: _("Retry verification")
-
- .col-sm-6
- = f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
- .input-group
- = text_field_tag :serverless_domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#serverless_domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
-
- .col-sm-12.form-text.text-muted
- = _("To access this domain create a new DNS record")
-
- .form-group
- = f.label :serverless_domain_verification, _('Verification status'), class: 'label-bold'
- .input-group
- = text_field_tag :serverless_domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#serverless_domain_verification', class: 'btn-default d-none d-sm-block')
- %p.form-text.text-muted
- - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
- = _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration.").html_safe % { link_to_help: link_to_help }
-
- - else
- .form-group
- = f.label :domain, _('Domain'), class: 'label-bold'
- = f.text_field :domain, class: 'form-control'
-
- - if show_certificate_card
- .card.js-domain-cert-show
- .card-header
- = _('Certificate')
- .d-flex.justify-content-between.align-items-center.p-3
- %span
- = @domain.subject || _('missing')
- %button.gl-button.btn.btn-danger.btn-sm.js-domain-cert-replace-btn{ type: 'button' }
- = _('Replace')
-
- .js-domain-cert-inputs{ class: ('hidden' if show_certificate_card) }
- .form-group
- = f.label :user_provided_certificate, _('Certificate (PEM)'), class: 'label-bold'
- = f.text_area :user_provided_certificate, rows: 5, class: 'form-control', value: ''
- %span.form-text.text-muted
- = _("Upload a certificate for your domain with all intermediates")
- .form-group
- = f.label :user_provided_key, _('Key (PEM)'), class: 'label-bold'
- = f.text_area :user_provided_key, rows: 5, class: 'form-control', value: ''
- %span.form-text.text-muted
- = _("Upload a private key for your certificate")
-
- = f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "gl-button btn btn-confirm js-serverless-domain-submit", disabled: @domain.persisted?
- - if @domain.persisted?
- %button.gl-button.btn.btn-danger{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
- = _('Delete domain')
-
--# haml-lint:disable NoPlainNodes
-- if @domain.persisted?
- - domain_attached = @domain.serverless_domain_clusters.count > 0
- .modal{ id: "modal-delete-domain", tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= _('Delete serverless domain?')
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": "true" } &times;
-
- .modal-body
- - if domain_attached
- = _("You must disassociate %{domain} from all clusters it is attached to before deletion.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
- - else
- = _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
-
- .modal-footer
- %a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }
- = _('Cancel')
-
- = link_to _('Delete domain'),
- admin_serverless_domain_path(@domain.id),
- title: _('Delete'),
- method: :delete,
- class: "gl-button btn btn-danger",
- disabled: domain_attached
diff --git a/app/views/admin/serverless/domains/index.html.haml b/app/views/admin/serverless/domains/index.html.haml
deleted file mode 100644
index c2b6baed4de..00000000000
--- a/app/views/admin/serverless/domains/index.html.haml
+++ /dev/null
@@ -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.")
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 13f2bf0019e..0a91194db51 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -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
diff --git a/config/feature_flags/development/pages_smart_check_outdated_sha.yml b/config/feature_flags/development/dependency_proxy_workhorse.yml
index 528d357f65c..a3545d32cd5 100644
--- a/config/feature_flags/development/pages_smart_check_outdated_sha.yml
+++ b/config/feature_flags/development/dependency_proxy_workhorse.yml
@@ -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
diff --git a/config/feature_flags/development/serverless_domain.yml b/config/feature_flags/development/serverless_domain.yml
deleted file mode 100644
index 67b2c6b8e1a..00000000000
--- a/config/feature_flags/development/serverless_domain.yml
+++ /dev/null
@@ -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
diff --git a/config/feature_flags/development/show_author_on_note.yml b/config/feature_flags/development/show_author_on_note.yml
deleted file mode 100644
index 7775bf5f27f..00000000000
--- a/config/feature_flags/development/show_author_on_note.yml
+++ /dev/null
@@ -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
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
index 6a9af7b4868..7d00776e460 100644
--- a/config/initializers/postgresql_cte.rb
+++ b/config/initializers/postgresql_cte.rb
@@ -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]
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index e3b365ad276..a17059c0265 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -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
diff --git a/config/routes/group.rb b/config/routes/group.rb
index ef31b639d33..803249f8861 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -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
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 0af0143a704..a9fd698a525 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -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.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9b4212e38fa..cb49f112c89 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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. |
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 2cbecc91b20..1a152db85b1 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -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:
-### Capture the image
+- Where they are in a complicated process.
+- How they should interact with the application.
-Use images to help the reader understand where they are in a process, or how
-they need to interact with the application.
+### Capture the image
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
diff --git a/doc/user/project/merge_requests/approvals/settings.md b/doc/user/project/merge_requests/approvals/settings.md
index ebd07f30f52..ea445216374 100644
--- a/doc/user/project/merge_requests/approvals/settings.md
+++ b/doc/user/project/merge_requests/approvals/settings.md
@@ -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)
diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb
index 61f35d0f784..5eaccbc7154 100644
--- a/lib/api/entities/group_detail.rb
+++ b/lib/api/entities/group_detail.rb
@@ -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
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index a1123b6291b..680e3a6e994 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -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)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index b937c5e26cb..e8a48d6c9f4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -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
diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb
index db46602cd90..a4bd06aec10 100644
--- a/lib/api/projects_relation_builder.rb
+++ b/lib/api/projects_relation_builder.rb
@@ -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
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 74fa98128e8..f16e1148618 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -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!
diff --git a/lib/error_tracking/sentry_client/issue.rb b/lib/error_tracking/sentry_client/issue.rb
index bdc567bd859..65da072ef8d 100644
--- a/lib/error_tracking/sentry_client/issue.rb
+++ b/lib/error_tracking/sentry_client/issue.rb
@@ -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
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
index d0b3fc176aa..d9ddb6caeec 100644
--- a/lib/gitlab/error_tracking/detailed_error.rb
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -22,6 +22,7 @@ module Gitlab
:gitlab_issue,
:gitlab_project,
:id,
+ :integrated,
:last_release_last_commit,
:last_release_short_version,
:last_release_version,
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 49be3ffc839..a047015e54f 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -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
diff --git a/lib/gitlab/performance_bar/stats.rb b/lib/gitlab/performance_bar/stats.rb
index 103cd65cb4b..a7a1bdb2ac6 100644
--- a/lib/gitlab/performance_bar/stats.rb
+++ b/lib/gitlab/performance_bar/stats.rb
@@ -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'])
diff --git a/lib/gitlab/sidekiq_config/dummy_worker.rb b/lib/gitlab/sidekiq_config/dummy_worker.rb
index 49696e913cf..8a2ea1acaab 100644
--- a/lib/gitlab/sidekiq_config/dummy_worker.rb
+++ b/lib/gitlab/sidekiq_config/dummy_worker.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/client.rb b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
index 4c33d2bfd31..7d3925e9dec 100644
--- a/lib/gitlab/sidekiq_middleware/worker_context/client.rb
+++ b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
@@ -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
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
index 78fa5009bc4..a44c6f85cf2 100644
--- a/lib/gitlab/subscription_portal.rb
+++ b/lib/gitlab/subscription_portal.rb
@@ -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
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 0f33c3aa68e..c5a99d4b93b 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -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' => {
diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb
index 110d78367b9..24e58e71023 100644
--- a/lib/sidebars/projects/menus/deployments_menu.rb
+++ b/lib/sidebars/projects/menus/deployments_menu.rb
@@ -27,7 +27,7 @@ module Sidebars
override :sprite_icon
def sprite_icon
- 'environment'
+ 'deployments'
end
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3459eb647b2..f8dff9c507b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index 65f0219fac9..c2fa1824653 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/qa/Gemfile b/qa/Gemfile
index 493e7de1e76..ee90d049d7b 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -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'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index ede0fbe00de..153a141d3fd 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -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)
diff --git a/spec/controllers/admin/instance_review_controller_spec.rb b/spec/controllers/admin/instance_review_controller_spec.rb
index d15894eeb5d..898cd30cdca 100644
--- a/spec/controllers/admin/instance_review_controller_spec.rb
+++ b/spec/controllers/admin/instance_review_controller_spec.rb
@@ -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
diff --git a/spec/controllers/admin/serverless/domains_controller_spec.rb b/spec/controllers/admin/serverless/domains_controller_spec.rb
deleted file mode 100644
index e7503fb37fa..00000000000
--- a/spec/controllers/admin/serverless/domains_controller_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 93491246e2c..e9a49319f21 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -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
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index 7415c2860c8..fa402d556c7 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -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
diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
index 56c0ef592ca..cc0f4a426f4 100644
--- a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
+++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
@@ -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) }
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 4e87a9fc1ba..6bcb88278a0 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -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
diff --git a/spec/factories/design_management/versions.rb b/spec/factories/design_management/versions.rb
index 247a385bd0e..e505a77d6bd 100644
--- a/spec/factories/design_management/versions.rb
+++ b/spec/factories/design_management/versions.rb
@@ -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
diff --git a/spec/features/admin/admin_serverless_domains_spec.rb b/spec/features/admin/admin_serverless_domains_spec.rb
deleted file mode 100644
index 0312e82e1ba..00000000000
--- a/spec/features/admin/admin_serverless_domains_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/features/groups/dependency_proxy_for_containers_spec.rb b/spec/features/groups/dependency_proxy_for_containers_spec.rb
new file mode 100644
index 00000000000..a4cd6d0f503
--- /dev/null
+++ b/spec/features/groups/dependency_proxy_for_containers_spec.rb
@@ -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
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index 9d8f9872a1a..e3a01ab6fa2 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -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
diff --git a/spec/fixtures/lib/gitlab/performance_bar/peek_data.json b/spec/fixtures/lib/gitlab/performance_bar/peek_data.json
index c60e787ddb1..5dade9a1a5d 100644
--- a/spec/fixtures/lib/gitlab/performance_bar/peek_data.json
+++ b/spec/fixtures/lib/gitlab/performance_bar/peek_data.json
@@ -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": {
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index babbc0c8a4d..4e459d800e8 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -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', () => {
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index 34094d22e68..ad4abda6912 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -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', () => {
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 0c88b6b1283..307323ef07a 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -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();
});
diff --git a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
index 8723c212486..09746750adc 100644
--- a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe GitlabSchema.types['SentryDetailedError'] do
it 'exposes the expected fields' do
expected_fields = %i[
id
+ integrated
sentryId
title
type
diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb
index e54296c58e0..82db0f70f2e 100644
--- a/spec/lib/error_tracking/sentry_client/issue_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb
@@ -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'] = [
diff --git a/spec/lib/gitlab/middleware/multipart/handler_spec.rb b/spec/lib/gitlab/middleware/multipart/handler_spec.rb
index aac3f00defe..53b59b042e2 100644
--- a/spec/lib/gitlab/middleware/multipart/handler_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart/handler_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb
index ad11eca56d1..b011f4e8346 100644
--- a/spec/lib/gitlab/performance_bar/stats_spec.rb
+++ b/spec/lib/gitlab/performance_bar/stats_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 7ef85cf25f6..92a11c83a4a 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index 628eb380396..5a2d5be3925 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -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
diff --git a/spec/models/error_tracking/error_spec.rb b/spec/models/error_tracking/error_spec.rb
index 5543392b624..9b8a81c6372 100644
--- a/spec/models/error_tracking/error_spec.rb
+++ b/spec/models/error_tracking/error_spec.rb
@@ -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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index fca99ebb856..024753ba516 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -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
diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb
index 2cd66f42458..d7b0ee888c0 100644
--- a/spec/models/namespace/traversal_hierarchy_spec.rb
+++ b/spec/models/namespace/traversal_hierarchy_spec.rb
@@ -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 }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9ed40ce0c8b..f192d2971cb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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) }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 38abedde7da..2c7e2ecff85 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -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
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ee1911b0a26..fb01845b63a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -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')
diff --git a/spec/routing/admin/serverless/domains_controller_routing_spec.rb b/spec/routing/admin/serverless/domains_controller_routing_spec.rb
deleted file mode 100644
index 60b60809f4d..00000000000
--- a/spec/routing/admin/serverless/domains_controller_routing_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 6d0b75e0c95..5810024a1ef 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -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
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index 148638fe5e7..bede30e1898 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -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
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 904b7efdd7f..dcaec176687 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -3,14 +3,30 @@
RSpec::Matchers.define_negated_matcher :be_nullable, :be_non_null
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
+ def permissions_for(klass)
+ if klass.respond_to?(:required_permissions)
+ klass.required_permissions
+ else
+ [klass.to_graphql.metadata[:authorize]]
+ end
+ end
+
match do |klass|
- permissions = if klass.respond_to?(:required_permissions)
- klass.required_permissions
- else
- [klass.to_graphql.metadata[:authorize]]
- end
+ 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
- expect(permissions).to eq(expected)
+ message = []
+ message << "is missing permissions: #{missing.inspect}" if missing.any?
+ message << "contained unexpected permissions: #{extra.inspect}" if extra.any?
+
+ message.join("\n")
end
end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index e8f7e62d0d7..30710e43357 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -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
diff --git a/workhorse/internal/api/api.go b/workhorse/internal/api/api.go
index 417ee71dbdc..7f696f70c7a 100644
--- a/workhorse/internal/api/api.go
+++ b/workhorse/internal/api/api.go
@@ -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
diff --git a/workhorse/internal/dependencyproxy/dependencyproxy.go b/workhorse/internal/dependencyproxy/dependencyproxy.go
new file mode 100644
index 00000000000..ebc310ca7f6
--- /dev/null
+++ b/workhorse/internal/dependencyproxy/dependencyproxy.go
@@ -0,0 +1,125 @@
+package dependencyproxy
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "time"
+
+ "gitlab.com/gitlab-org/labkit/correlation"
+ "gitlab.com/gitlab-org/labkit/log"
+ "gitlab.com/gitlab-org/labkit/tracing"
+
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/senddata"
+)
+
+// httpTransport defines a http.Transport with values
+// that are more restrictive than for http.DefaultTransport,
+// they define shorter TLS Handshake, and more aggressive connection closing
+// to prevent the connection hanging and reduce FD usage
+var httpTransport = tracing.NewRoundTripper(correlation.NewInstrumentedRoundTripper(&http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 10 * time.Second,
+ }).DialContext,
+ MaxIdleConns: 2,
+ IdleConnTimeout: 30 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 10 * time.Second,
+ ResponseHeaderTimeout: 30 * time.Second,
+}))
+
+var httpClient = &http.Client{
+ Transport: httpTransport,
+}
+
+type Injector struct {
+ senddata.Prefix
+ uploadHandler http.Handler
+}
+
+type entryParams struct {
+ Url string
+ Header http.Header
+}
+
+type nullResponseWriter struct {
+ header http.Header
+ status int
+}
+
+func (nullResponseWriter) Write(p []byte) (int, error) {
+ return len(p), nil
+}
+
+func (w *nullResponseWriter) Header() http.Header {
+ return w.header
+}
+
+func (w *nullResponseWriter) WriteHeader(status int) {
+ if w.status == 0 {
+ w.status = status
+ }
+}
+
+func NewInjector() *Injector {
+ return &Injector{
+ Prefix: "send-dependency:",
+ }
+}
+
+func (p *Injector) SetUploadHandler(uploadHandler http.Handler) {
+ p.uploadHandler = uploadHandler
+}
+
+func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData string) {
+ dependencyResponse, err := p.fetchUrl(r.Context(), sendData)
+ if err != nil {
+ helper.Fail500(w, r, err)
+ return
+ }
+ defer dependencyResponse.Body.Close()
+ if dependencyResponse.StatusCode >= 400 {
+ w.WriteHeader(dependencyResponse.StatusCode)
+ io.Copy(w, dependencyResponse.Body)
+ return
+ }
+
+ teeReader := io.TeeReader(dependencyResponse.Body, w)
+ saveFileRequest, err := http.NewRequestWithContext(r.Context(), "POST", r.URL.String()+"/upload", teeReader)
+ if err != nil {
+ helper.Fail500(w, r, fmt.Errorf("dependency proxy: failed to create request: %w", err))
+ }
+ saveFileRequest.Header = helper.HeaderClone(r.Header)
+ saveFileRequest.ContentLength = dependencyResponse.ContentLength
+
+ w.Header().Del("Content-Length")
+
+ nrw := &nullResponseWriter{header: http.Header{}}
+ p.uploadHandler.ServeHTTP(nrw, saveFileRequest)
+
+ if nrw.status != http.StatusOK {
+ fields := log.Fields{"code": nrw.status}
+
+ helper.Fail500WithFields(nrw, r, fmt.Errorf("dependency proxy: failed to upload file"), fields)
+ }
+}
+
+func (p *Injector) fetchUrl(ctx context.Context, sendData string) (*http.Response, error) {
+ var params entryParams
+ if err := p.Unpack(&params, sendData); err != nil {
+ return nil, fmt.Errorf("dependency proxy: unpack sendData: %v", err)
+ }
+
+ r, err := http.NewRequestWithContext(ctx, "GET", params.Url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("dependency proxy: failed to fetch dependency: %v", err)
+ }
+ r.Header = params.Header
+
+ return httpClient.Do(r)
+}
diff --git a/workhorse/internal/dependencyproxy/dependencyproxy_test.go b/workhorse/internal/dependencyproxy/dependencyproxy_test.go
new file mode 100644
index 00000000000..395ca58f90e
--- /dev/null
+++ b/workhorse/internal/dependencyproxy/dependencyproxy_test.go
@@ -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
+}
diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go
index 8c85c5144e5..9e92393dcaa 100644
--- a/workhorse/internal/upstream/routes.go
+++ b/workhorse/internal/upstream/routes.go
@@ -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,
diff --git a/workhorse/main_test.go b/workhorse/main_test.go
index 6e61e2fc65a..f90a07f1d7d 100644
--- a/workhorse/main_test.go
+++ b/workhorse/main_test.go
@@ -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")
+ })
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index f5e4ff2b752..20dc6127116 100644
--- a/yarn.lock
+++ b/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/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/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.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"