diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-05 15:12:53 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-05 15:12:53 +0000 |
commit | a84626f13d61d190b2db5e44caf71b22fc541276 (patch) | |
tree | 5cf591ce134ac0ad5b8c101e3518b2e49101b6ad | |
parent | c9b0dfef1ba43a9e04264023b08c589bcb9eb397 (diff) | |
download | gitlab-ce-a84626f13d61d190b2db5e44caf71b22fc541276.tar.gz |
Add latest changes from gitlab-org/gitlab@master
90 files changed, 1386 insertions, 1161 deletions
@@ -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" } × - - .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(¶ms, sendData); err != nil { + return nil, fmt.Errorf("dependency proxy: unpack sendData: %v", err) + } + + r, err := http.NewRequestWithContext(ctx, "GET", params.Url, nil) + if err != nil { + return nil, fmt.Errorf("dependency proxy: failed to fetch dependency: %v", err) + } + r.Header = params.Header + + return httpClient.Do(r) +} 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" |