diff options
77 files changed, 1302 insertions, 160 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index e5f6baa1623..e71e74fd4d3 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -7,11 +7,12 @@ *.rake @gitlab-org/maintainers/rails-backend # Technical writing team are the default reviewers for all markdown docs -*.md @gl-docsteam /doc/ @gl-docsteam # Dev and Doc guidelines /doc/development/ @marcia @mjang1 /doc/development/documentation/ @mikelewis +/doc/ci @marcel.amirault @sselhorn +/doc/.linting @marcel.amirault @eread @aqualls @mikelewis # Frontend maintainers should see everything in `app/assets/` *.scss @annabeldunstone @gitlab-org/maintainers/frontend diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index ace48f25716..d042336c361 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -151,12 +151,14 @@ export default { <strong>{{ $options.severityLabels[alert.severity] }}</strong> </div> <span class="mx-2">•</span> - <gl-sprintf :message="reportedAtMessage"> - <template #when> - <time-ago-tooltip :time="alert.createdAt" class="gl-ml-3" /> - </template> - <template #tool>{{ alert.monitoringTool }}</template> - </gl-sprintf> + <span> + <gl-sprintf :message="reportedAtMessage"> + <template #when> + <time-ago-tooltip :time="alert.createdAt" /> + </template> + <template #tool>{{ alert.monitoringTool }}</template> + </gl-sprintf> + </span> </div> <gl-button v-if="glFeatures.createIssueFromAlertEnabled" diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue index 7d5d48cfc31..4f433bd8dfd 100644 --- a/app/assets/javascripts/blob/components/blob_content.vue +++ b/app/assets/javascripts/blob/components/blob_content.vue @@ -3,12 +3,19 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; import BlobContentError from './blob_content_error.vue'; +import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from './constants'; + export default { components: { GlLoadingIcon, BlobContentError, }, props: { + blob: { + type: Object, + required: false, + default: () => ({}), + }, content: { type: String, default: '', @@ -37,6 +44,8 @@ export default { return this.activeViewer.renderError; }, }, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, }; </script> <template> @@ -44,7 +53,13 @@ export default { <gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" /> <template v-else> - <blob-content-error v-if="viewerError" :viewer-error="viewerError" /> + <blob-content-error + v-if="viewerError" + :viewer-error="viewerError" + :blob="blob" + @[$options.BLOB_RENDER_EVENT_LOAD]="$emit($options.BLOB_RENDER_EVENT_LOAD)" + @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="$emit($options.BLOB_RENDER_EVENT_SHOW_SOURCE)" + /> <component :is="viewer" v-else diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue index 0f1af0a962d..44dc4a6c727 100644 --- a/app/assets/javascripts/blob/components/blob_content_error.vue +++ b/app/assets/javascripts/blob/components/blob_content_error.vue @@ -1,15 +1,84 @@ <script> +import { __ } from '~/locale'; +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { BLOB_RENDER_ERRORS } from './constants'; + export default { + components: { + GlSprintf, + GlLink, + }, props: { viewerError: { type: String, required: true, }, + blob: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + notStoredExternally() { + return this.viewerError !== BLOB_RENDER_ERRORS.REASONS.EXTERNAL.id; + }, + renderErrorReason() { + const defaultReasonPath = Object.keys(BLOB_RENDER_ERRORS.REASONS).find( + reason => BLOB_RENDER_ERRORS.REASONS[reason].id === this.viewerError, + ); + const defaultReason = BLOB_RENDER_ERRORS.REASONS[defaultReasonPath].text; + return this.notStoredExternally + ? defaultReason + : defaultReason[this.blob.externalStorage || 'default']; + }, + renderErrorOptions() { + const load = { + ...BLOB_RENDER_ERRORS.OPTIONS.LOAD, + condition: this.shouldShowLoadBtn, + }; + const showSource = { + ...BLOB_RENDER_ERRORS.OPTIONS.SHOW_SOURCE, + condition: this.shouldShowSourceBtn, + }; + const download = { + ...BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD, + href: this.blob.rawPath, + }; + return [load, showSource, download]; + }, + shouldShowLoadBtn() { + return this.viewerError === BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id; + }, + shouldShowSourceBtn() { + return this.blob.richViewer && this.blob.renderedAsText && this.notStoredExternally; + }, }, + errorMessage: __( + 'This content could not be displayed because %{reason}. You can %{options} instead.', + ), }; </script> <template> <div class="file-content code"> - <div class="text-center py-4" v-html="viewerError"></div> + <div class="text-center py-4"> + <gl-sprintf :message="$options.errorMessage"> + <template #reason>{{ renderErrorReason }}</template> + <template #options> + <template v-for="option in renderErrorOptions"> + <span v-if="option.condition" :key="option.text"> + <gl-link + :href="option.href" + :target="option.target" + :data-test-id="`option-${option.id}`" + @click="option.event && $emit(option.event)" + >{{ option.text }}</gl-link + > + {{ option.conjunction }} + </span> + </template> + </template> + </gl-sprintf> + </div> </div> </template> diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js index d3fed9e51e9..93dceacabdd 100644 --- a/app/assets/javascripts/blob/components/constants.js +++ b/app/assets/javascripts/blob/components/constants.js @@ -1,4 +1,5 @@ -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents'); export const BTN_RAW_TITLE = __('Open raw'); @@ -9,3 +10,56 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source'); export const RICH_BLOB_VIEWER = 'rich'; export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file'); + +export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch'; +export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer'; + +export const BLOB_RENDER_ERRORS = { + REASONS: { + COLLAPSED: { + id: 'collapsed', + text: sprintf(__('it is larger than %{limit}'), { + limit: numberToHumanSize(1048576), // 1MB in bytes + }), + }, + TOO_LARGE: { + id: 'too_large', + text: sprintf(__('it is larger than %{limit}'), { + limit: numberToHumanSize(104857600), // 100MB in bytes + }), + }, + EXTERNAL: { + id: 'server_side_but_stored_externally', + text: { + lfs: __('it is stored in LFS'), + build_artifact: __('it is stored as a job artifact'), + default: __('it is stored externally'), + }, + }, + }, + OPTIONS: { + LOAD: { + id: 'load', + text: __('load it anyway'), + conjunction: __('or'), + href: '#', + target: '', + event: BLOB_RENDER_EVENT_LOAD, + }, + SHOW_SOURCE: { + id: 'show_source', + text: __('view the source'), + conjunction: __('or'), + href: '#', + target: '', + event: BLOB_RENDER_EVENT_SHOW_SOURCE, + }, + DOWNLOAD: { + id: 'download', + text: __('download it'), + conjunction: '', + target: '_blank', + condition: true, + }, + }, +}; diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js index 7891b44dd27..4f04a1b8c16 100644 --- a/app/assets/javascripts/helpers/avatar_helper.js +++ b/app/assets/javascripts/helpers/avatar_helper.js @@ -1,11 +1,14 @@ import { escape } from 'lodash'; import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; export const DEFAULT_SIZE_CLASS = 's40'; export const IDENTICON_BG_COUNT = 7; export function getIdenticonBackgroundClass(entityId) { - const type = (entityId % IDENTICON_BG_COUNT) + 1; + // If a GraphQL string id is passed in, convert it to the entity number + const id = typeof entityId === 'string' ? getIdFromGraphQLId(entityId) : entityId; + const type = (id % IDENTICON_BG_COUNT) + 1; return `bg${type}`; } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 05869b483c8..713f57a2b27 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -111,6 +111,9 @@ function deferredInitialisation() { const recoverySettingsCallout = document.querySelector('.js-recovery-settings-callout'); PersistentUserCallout.factory(recoverySettingsCallout); + const usersOverLicenseCallout = document.querySelector('.js-users-over-license-callout'); + PersistentUserCallout.factory(usersOverLicenseCallout); + if (document.querySelector('.search')) initSearchAutocomplete(); addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index 4598626718c..b3068c46bcb 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -18,6 +18,11 @@ export default class PersistentUserCallout { init() { const closeButton = this.container.querySelector('.js-close'); + + if (!closeButton) { + return; + } + closeButton.addEventListener('click', event => this.dismiss(event)); if (this.deferLinks) { diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index d615eaadb78..6af1c161c5e 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -7,7 +7,12 @@ import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import GetBlobContent from '../queries/snippet.blob.content.query.graphql'; -import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; +import { + SIMPLE_BLOB_VIEWER, + RICH_BLOB_VIEWER, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, +} from '~/blob/components/constants'; export default { components: { @@ -27,6 +32,16 @@ export default { }, update: data => data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData, + result() { + if (this.activeViewerType === RICH_BLOB_VIEWER) { + this.blob.richViewer.renderError = null; + } else { + this.blob.simpleViewer.renderError = null; + } + }, + skip() { + return this.viewer.renderError; + }, }, }, props: { @@ -62,9 +77,15 @@ export default { }, methods: { switchViewer(newViewer) { - this.activeViewerType = newViewer; + this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER; + }, + forceQuery() { + this.$apollo.queries.blobContent.skip = false; + this.$apollo.queries.blobContent.refetch(); }, }, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, }; </script> <template> @@ -81,7 +102,14 @@ export default { /> </template> </blob-header> - <blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" /> + <blob-content + :loading="isContentLoading" + :content="blobContent" + :active-viewer="viewer" + :blob="blob" + @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery" + @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer" + /> </article> </div> </template> diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql index d793d0b6bb4..e7765dfd8ba 100644 --- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql +++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql @@ -17,6 +17,8 @@ fragment SnippetBase on Snippet { path rawPath size + externalStorage + renderedAsText simpleViewer { ...BlobViewer } diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 9dd61c8eada..87a995464fa 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -4,7 +4,7 @@ import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar export default { props: { entityId: { - type: Number, + type: [Number, String], required: true, }, entityName: { diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb index ed0995e7ffd..5723ccc14a7 100644 --- a/app/controllers/google_api/authorizations_controller.rb +++ b/app/controllers/google_api/authorizations_controller.rb @@ -15,6 +15,9 @@ module GoogleApi session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = expires_at.to_s + rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed + flash[:alert] = _('Timeout connecting to the Google API. Please try again.') + ensure redirect_to redirect_uri_from_session end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a815b378f8b..2df33073a89 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -54,6 +54,10 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == action_name } end + def admin_section? + controller.class.ancestors.include?(Admin::ApplicationController) + end + def last_commit(project) if project.repo_exists? time_ago_with_tooltip(project.repository.commit.committed_date) diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 1ed97ada412..83f558af1a1 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -223,11 +223,19 @@ module Clusters end def applications - APPLICATIONS_ASSOCIATIONS.map do |association_name| - public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend + APPLICATIONS.each_value.map do |application_class| + find_or_build_application(application_class) end end + def find_or_build_application(application_class) + raise ArgumentError, "#{application_class} is not in APPLICATIONS" unless APPLICATIONS.value?(application_class) + + association_name = application_class.association_name + + public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend + end + def provider if gcp? provider_gcp diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 14237439a8d..0b915126f8a 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -27,6 +27,7 @@ module Clusters state :update_errored, value: 6 state :uninstalling, value: 7 state :uninstall_errored, value: 8 + state :uninstalled, value: 10 # Used for applications that are pre-installed by the cluster, # e.g. Knative in GCP Cloud Run enabled clusters @@ -35,6 +36,14 @@ module Clusters # and no exit transitions. state :pre_installed, value: 9 + event :make_externally_installed do + transition any => :installed + end + + event :make_externally_uninstalled do + transition any => :uninstalled + end + event :make_scheduled do transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled end diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index c2b7632971a..f0ffe67510b 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -61,6 +61,7 @@ module Ci case artifact.file_type when 'dotenv' then parse_dotenv_artifact(job, artifact) + when 'cluster_applications' then parse_cluster_applications_artifact(job, artifact) else success end end @@ -111,5 +112,9 @@ module Ci def parse_dotenv_artifact(job, artifact) Ci::ParseDotenvArtifactService.new(job.project, current_user).execute(artifact) end + + def parse_cluster_applications_artifact(job, artifact) + Clusters::ParseClusterApplicationsArtifactService.new(job, job.user).execute(artifact) + end end end diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb new file mode 100644 index 00000000000..b8e1c80cfe7 --- /dev/null +++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Clusters + class ParseClusterApplicationsArtifactService < ::BaseService + include Gitlab::Utils::StrongMemoize + + MAX_ACCEPTABLE_ARTIFACT_SIZE = 5.kilobytes + RELEASE_NAMES = %w[prometheus].freeze + + def initialize(job, current_user) + @job = job + + super(job.project, current_user) + end + + def execute(artifact) + return success unless Feature.enabled?(:cluster_applications_artifact, project) + + raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications? + + unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE + return error(too_big_error_message, :bad_request) + end + + unless cluster + return error(s_('ClusterIntegration|No deployment cluster found for this job')) + end + + parse!(artifact) + + success + rescue Gitlab::Kubernetes::Helm::Parsers::ListV2::ParserError, ActiveRecord::RecordInvalid => error + Gitlab::ErrorTracking.track_exception(error, job_id: artifact.job_id) + error(error.message, :bad_request) + end + + private + + attr_reader :job + + def cluster + strong_memoize(:cluster) do + deployment_cluster = job.deployment&.cluster + + deployment_cluster if Ability.allowed?(current_user, :admin_cluster, deployment_cluster) + end + end + + def parse!(artifact) + releases = [] + + artifact.each_blob do |blob| + releases.concat(Gitlab::Kubernetes::Helm::Parsers::ListV2.new(blob).releases) + end + + update_cluster_application_statuses!(releases) + end + + def update_cluster_application_statuses!(releases) + release_by_name = releases.index_by { |release| release['Name'] } + + Clusters::Cluster.transaction do + RELEASE_NAMES.each do |release_name| + application = find_or_build_application(release_name) + + release = release_by_name[release_name] + + if release + case release['Status'] + when 'DEPLOYED' + application.make_externally_installed! + when 'FAILED' + application.make_errored!(s_('ClusterIntegration|Helm release failed to install')) + end + else + # missing, so by definition, we consider this uninstalled + application.make_externally_uninstalled! if application.persisted? + end + end + end + end + + def find_or_build_application(application_name) + application_class = Clusters::Cluster::APPLICATIONS[application_name] + + cluster.find_or_build_application(application_class) + end + + def too_big_error_message + human_size = ActiveSupport::NumberHelper.number_to_human_size(MAX_ACCEPTABLE_ARTIFACT_SIZE) + + s_('ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}') % { human_size: human_size } + end + end +end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 49345b7b215..3885fa311ba 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -5,6 +5,7 @@ .mobile-overlay .alert-wrapper = render 'shared/outdated_browser' + = render_if_exists 'layouts/header/users_over_license_banner' - if Feature.enabled?(:subscribable_banner_license, default_enabled: true) = render_if_exists "layouts/header/ee_subscribable_banner" = render "layouts/broadcast" diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml index c6629cd33a5..25c841d2344 100644 --- a/app/views/shared/_delete_label_modal.html.haml +++ b/app/views/shared/_delete_label_modal.html.haml @@ -2,20 +2,19 @@ .modal-dialog .modal-content .modal-header - %h3.page-title Delete label: #{label.name} ? + %h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %span{ "aria-hidden": true } × .modal-body %p - %strong= label.name - %span will be permanently deleted from #{label.subject_name}. This cannot be undone. + = _('<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>').html_safe % { label_name: label.name, subject_name: label.subject_name } .modal-footer - %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel + %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel') - = link_to 'Delete label', + = link_to _('Delete label'), label.destroy_path, - title: 'Delete', + title: _('Delete'), method: :delete, class: 'btn btn-remove' diff --git a/bin/background_jobs b/bin/background_jobs index 598d36abde6..866f5c39cd6 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd $(dirname $0)/.. diff --git a/bin/background_jobs_sk b/bin/background_jobs_sk index 131cfe116ff..fb7de0a6180 100755 --- a/bin/background_jobs_sk +++ b/bin/background_jobs_sk @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd $(dirname $0)/.. app_root=$(pwd) diff --git a/bin/background_jobs_sk_cluster b/bin/background_jobs_sk_cluster index 1982fd0810d..b1d5fce204e 100755 --- a/bin/background_jobs_sk_cluster +++ b/bin/background_jobs_sk_cluster @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd $(dirname $0)/.. app_root=$(pwd) diff --git a/changelogs/unreleased/216453-large-files-snippet.yml b/changelogs/unreleased/216453-large-files-snippet.yml new file mode 100644 index 00000000000..5e0f5dd6009 --- /dev/null +++ b/changelogs/unreleased/216453-large-files-snippet.yml @@ -0,0 +1,5 @@ +--- +title: Refactored render errors for blob to Vue +merge_request: 32345 +author: +type: changed diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml new file mode 100644 index 00000000000..22a328cb51a --- /dev/null +++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml @@ -0,0 +1,5 @@ +--- +title: Externalize i18n strings from ./app/views/shared/_delete_label_modal.html.haml +merge_request: 32138 +author: Gilang Gumilar +type: changed diff --git a/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml b/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml new file mode 100644 index 00000000000..915322c3734 --- /dev/null +++ b/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml @@ -0,0 +1,5 @@ +--- +title: Improve responses in the snippet create/update API endpoints +merge_request: 32282 +author: +type: fixed diff --git a/changelogs/unreleased/dblessing-google-oauth2-timeout.yml b/changelogs/unreleased/dblessing-google-oauth2-timeout.yml new file mode 100644 index 00000000000..a4cdc3817c2 --- /dev/null +++ b/changelogs/unreleased/dblessing-google-oauth2-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Set timeout for Google OAuth to prevent 503 error +merge_request: 30653 +author: +type: fixed diff --git a/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml b/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml new file mode 100644 index 00000000000..4d50cd8e3a2 --- /dev/null +++ b/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml @@ -0,0 +1,5 @@ +--- +title: Add nested file detection for Dependency Scanning +merge_request: 31932 +author: +type: fixed diff --git a/changelogs/unreleased/tr-fix-space-char.yml b/changelogs/unreleased/tr-fix-space-char.yml new file mode 100644 index 00000000000..19f3a47c8a8 --- /dev/null +++ b/changelogs/unreleased/tr-fix-space-char.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing space character in alert header +merge_request: 32395 +author: +type: fixed diff --git a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb new file mode 100644 index 00000000000..760fcba5935 --- /dev/null +++ b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module OmniAuth + module Strategies + class OAuth2 + alias_method :original_callback_phase, :callback_phase + + # Monkey patch until PR is merged and released upstream + # https://github.com/omniauth/omniauth-oauth2/pull/129 + def callback_phase + original_callback_phase + rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed => e + fail!(:timeout, e) + end + end + end +end diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 8c78cd66b3f..14ae0c3e8c6 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -18,21 +18,22 @@ You can read more about the Docker Registry at **Omnibus GitLab installations** -If you are using the Omnibus GitLab built-in [Let's Encrypt integration](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), as of GitLab 12.5, the Container Registry will be automatically enabled on port 5050 of the default domain. +If you installed GitLab by using the Omnibus installation package, the Container Registry +may or may not be available by default. -If you are not using GitLab 12.5 or later, or do not use GitLab's built-in Let's Encrypt -integration, the GitLab Container Registry must be enabled and -[configured to use an external domain](#container-registry-domain-configuration). +The Container Registry is automatically enabled and available on your GitLab domain, port 5050 if: -To enable the GitLab Container Registry on your *existing* GitLab domain, refer to the section on -[configuring Container Registry to use an existing domain](#configure-container-registry-under-an-existing-gitlab-domain). +- You're using the built-in [Let's Encrypt integration](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), and +- You're using GitLab 12.5 or later. -To use a *separate* domain with your Container Registry, refer to the section on -[configuring Container Registry under its own domain](#configure-container-registry-under-its-own-domain). +Otherwise, the Container Registry is not enabled. To enable it: + +- You can configure it for your [GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain), or +- You can configure it for [a different domain](#configure-container-registry-under-its-own-domain). NOTE: **Note:** -The container registry works under HTTPS by default. Using HTTP is possible -but not recommended and out of the scope of this document. +The Container Registry works under HTTPS by default. You can use HTTP +but it's not recommended and is out of the scope of this document. Read the [insecure Registry documentation](https://docs.docker.com/registry/insecure/) if you want to implement this. diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md index 99d01590895..587f1f91f72 100644 --- a/doc/ci/docker/using_kaniko.md +++ b/doc/ci/docker/using_kaniko.md @@ -82,6 +82,7 @@ store: ```yaml before_script: + - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - | echo "-----BEGIN CERTIFICATE----- diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 64ee2940eee..3a8e08291e1 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3617,7 +3617,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya YAML has a handy feature called 'anchors', which lets you easily duplicate content across your document. Anchors can be used to duplicate/inherit -properties, and is a perfect example to be used with [hidden keys](#hide-jobs) +properties, and is a perfect example to be used with [hidden jobs](#hide-jobs) to provide templates for your jobs. The following example uses anchors and map merging. It will create two jobs, @@ -3731,7 +3731,7 @@ test:mysql: - ruby ``` -You can see that the hidden keys are conveniently used as templates. +You can see that the hidden jobs are conveniently used as templates. NOTE: **Note:** You can't use YAML anchors across multiple files when leveraging the [`include`](#include) @@ -3829,7 +3829,7 @@ GitLab CI/CD. In the following example, `.hidden_job` will be ignored: ``` Use this feature to ignore jobs, or use the -[special YAML features](#special-yaml-features) and transform the hidden keys +[special YAML features](#special-yaml-features) and transform the hidden jobs into templates. ## Skip Pipeline diff --git a/doc/development/code_review.md b/doc/development/code_review.md index e49ad23d822..a5ad7dc0f46 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -327,6 +327,23 @@ Before taking the decision to merge: before merging. A comment must to be posted if the MR is merged with any failed job. - If the MR contains both Quality and non-Quality-related changes, the MR should be merged by the relevant maintainer for user-facing changes (backend, frontend, or database) after the Quality related changes are approved by a Software Engineer in Test. +If a merge request is fundamentally ready, but needs only trivial fixes (such as +typos), consider demonstrating a [bias for +action](https://about.gitlab.com/handbook/values/#bias-for-action) by making +those changes directly without going back to the author. You can do this by +using the [suggest changes](../user/discussions/index.md#suggest-changes) feature to apply +your own suggestions to the merge request. Note that: + +- If the changes are not straightforward, please prefer assigning the merge request back + to the author. +- **Before applying suggestions**, edit the merge request to make sure + [squash and + merge](../user/project/merge_requests/squash_and_merge.md#squash-and-merge) + is enabled, otherwise, the pipeline's Danger job will fail. + - If a merge request does not have squash and merge enabled, and it + has more than one commit, then see the note below about rewriting + commit history. + When ready to merge: - Consider using the [Squash and diff --git a/doc/raketasks/x509_signatures.md b/doc/raketasks/x509_signatures.md index e52584680f4..f7c47794690 100644 --- a/doc/raketasks/x509_signatures.md +++ b/doc/raketasks/x509_signatures.md @@ -1,5 +1,7 @@ # X.509 signatures **(CORE ONLY)** +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122159) in GitLab 12.10. + When [signing commits with X.509](../user/project/repository/x509_signed_commits/index.md), the trust anchor might change and the signatures stored within the database must be updated. @@ -10,13 +12,13 @@ certificate store. To update all X.509 signatures, run: -**Omnibus Installation** +**Omnibus Installations:** ```shell sudo gitlab-rake gitlab:x509:update_signatures ``` -**Source Installation** +**Source Installations:** ```shell sudo -u git -H bundle exec rake gitlab:x509:update_signatures RAILS_ENV=production diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index b94def57c76..50eb0c64f4b 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -217,9 +217,9 @@ To reorder child epics assigned to an epic: 1. Go to the **Epics and Issues** tab. 1. Drag and drop epics into the desired order. -### Move issues between epics +### Move issues between epics **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in GitLab 13.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. New issues are added to the top of their list in the **Epics and Issues** tab. You can move issues from one epic to another. Issues and child epics cannot be intermingled. @@ -270,14 +270,14 @@ To add a child epic to an epic: ### Move child epics between epics -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in GitLab 13.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. New child epics are added to the top of their list in the **Epics and Issues** tab. You can move child epics from one epic to another. When you add an epic that's already linked to a parent epic, the link to its current parent is removed. Issues and child epics cannot be intermingled. -To move child epics **(ULTIMATE)** to another epic: +To move child epics to another epic: 1. Go to the **Epics and Issues** tab. 1. Drag and drop epics into the desired parent epic. diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 675536da2eb..e37c1d9d319 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -70,12 +70,12 @@ module API service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute snippet = service_response.payload[:snippet] - render_spam_error! if snippet.spam? - - if snippet.persisted? + if service_response.success? present snippet, with: Entities::ProjectSnippet else - render_validation_error!(snippet) + render_spam_error! if snippet.spam? + + render_api_error!({ error: service_response.message }, service_response.http_status) end end @@ -106,12 +106,12 @@ module API service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet) snippet = service_response.payload[:snippet] - render_spam_error! if snippet.spam? - - if snippet.valid? + if service_response.success? present snippet, with: Entities::ProjectSnippet else - render_validation_error!(snippet) + render_spam_error! if snippet.spam? + + render_api_error!({ error: service_response.message }, service_response.http_status) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index b89de93af1b..b2719f36081 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -81,12 +81,12 @@ module API service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute snippet = service_response.payload[:snippet] - render_spam_error! if snippet.spam? - - if snippet.persisted? + if service_response.success? present snippet, with: Entities::PersonalSnippet else - render_validation_error!(snippet) + render_spam_error! if snippet.spam? + + render_api_error!({ error: service_response.message }, service_response.http_status) end end @@ -115,12 +115,12 @@ module API service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet) snippet = service_response.payload[:snippet] - render_spam_error! if snippet.spam? - - if snippet.persisted? + if service_response.success? present snippet, with: Entities::PersonalSnippet else - render_validation_error!(snippet) + render_spam_error! if snippet.spam? + + render_api_error!({ error: service_response.message }, service_response.http_status) end end diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb index f0811098b15..6d699d37a8c 100644 --- a/lib/gitlab/auth/o_auth/provider.rb +++ b/lib/gitlab/auth/o_auth/provider.rb @@ -66,7 +66,10 @@ module Gitlab nil end else - Gitlab.config.omniauth.providers.find { |provider| provider.name == name } + provider = Gitlab.config.omniauth.providers.find { |provider| provider.name == name } + merge_provider_args_with_defaults!(provider) + + provider end end @@ -81,6 +84,15 @@ module Gitlab config = config_for(name) config && config['icon'] end + + def self.merge_provider_args_with_defaults!(provider) + return unless provider + + provider['args'] ||= {} + + defaults = Gitlab::OmniauthInitializer.default_arguments_for(provider['name']) + provider['args'].deep_merge!(defaults.deep_stringify_keys) + end end end end diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index d99cb171b99..616966b4f04 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -105,13 +105,13 @@ gemnasium-dependency_scanning: $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $DS_DEFAULT_ANALYZERS =~ /gemnasium([^-]|$)/ exists: - - 'Gemfile.lock' - - 'composer.lock' - - 'gems.locked' - - 'go.sum' - - 'npm-shrinkwrap.json' - - 'package-lock.json' - - 'yarn.lock' + - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' + - '{composer.lock,*/composer.lock,*/*/composer.lock}' + - '{gems.locked,*/gems.locked,*/*/gems.locked}' + - '{go.sum,*/go.sum,*/*/go.sum}' + - '{npm-shrinkwrap.json,*/npm-shrinkwrap.json,*/*/npm-shrinkwrap.json}' + - '{package-lock.json,*/package-lock.json,*/*/package-lock.json}' + - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}' gemnasium-maven-dependency_scanning: extends: .ds-analyzer @@ -124,9 +124,9 @@ gemnasium-maven-dependency_scanning: $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/ exists: - - 'build.gradle' - - 'build.sbt' - - 'pom.xml' + - '{build.gradle,*/build.gradle,*/*/build.gradle}' + - '{build.sbt,*/build.sbt,*/*/build.sbt}' + - '{pom.xml,*/pom.xml,*/*/pom.xml}' gemnasium-python-dependency_scanning: extends: .ds-analyzer @@ -139,11 +139,11 @@ gemnasium-python-dependency_scanning: $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ exists: - - 'requirements.txt' - - 'requirements.pip' - - 'Pipfile' - - 'requires.txt' - - 'setup.py' + - '{requirements.txt,*/requirements.txt,*/*/requirements.txt}' + - '{requirements.pip,*/requirements.pip,*/*/requirements.pip}' + - '{Pipfile,*/Pipfile,*/*/Pipfile}' + - '{requires.txt,*/requires.txt,*/*/requires.txt}' + - '{setup.py,*/setup.py,*/*/setup.py}' # Support passing of $PIP_REQUIREMENTS_FILE # See https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning - if: $CI_COMMIT_BRANCH && @@ -162,7 +162,7 @@ bundler-audit-dependency_scanning: $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $DS_DEFAULT_ANALYZERS =~ /bundler-audit/ exists: - - 'Gemfile.lock' + - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' retire-js-dependency_scanning: extends: .ds-analyzer @@ -175,4 +175,4 @@ retire-js-dependency_scanning: $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $DS_DEFAULT_ANALYZERS =~ /retire.js/ exists: - - 'package.json' + - '{package.json,*/package.json,*/*/package.json}' diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb index 616c05d0a02..58db2b58560 100644 --- a/lib/gitlab/danger/commit_linter.rb +++ b/lib/gitlab/danger/commit_linter.rb @@ -18,7 +18,7 @@ module Gitlab PROBLEMS = { subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words", subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters", - subject_above_warning: "The %s length is acceptable, but please try to [reduce it to #{WARN_SUBJECT_LENGTH} characters](#{URL_LIMIT_SUBJECT}).", + subject_above_warning: "The %s length is acceptable, but please try to [reduce it to #{WARN_SUBJECT_LENGTH} characters](#{URL_LIMIT_SUBJECT})", subject_starts_with_lowercase: "The %s must start with a capital letter", subject_ends_with_a_period: "The %s must not end with a period", separator_missing: "The commit subject and body must be separated by a blank line", diff --git a/lib/gitlab/kubernetes/helm/parsers/list_v2.rb b/lib/gitlab/kubernetes/helm/parsers/list_v2.rb index daa716cdef7..c5c5d198a6c 100644 --- a/lib/gitlab/kubernetes/helm/parsers/list_v2.rb +++ b/lib/gitlab/kubernetes/helm/parsers/list_v2.rb @@ -18,7 +18,17 @@ module Gitlab end def releases - @releases ||= json["Releases"] || [] + @releases = helm_releases + end + + private + + def helm_releases + helm_releases = json['Releases'] || [] + + raise ParserError, 'Invalid format for Releases' unless helm_releases.all? { |item| item.is_a?(Hash) } + + helm_releases end end end diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb index 4a7a7709c79..b60ecb6631b 100644 --- a/lib/gitlab/omniauth_initializer.rb +++ b/lib/gitlab/omniauth_initializer.rb @@ -2,6 +2,8 @@ module Gitlab class OmniauthInitializer + OAUTH2_TIMEOUT_SECONDS = 10 + def initialize(devise_config) @devise_config = devise_config end @@ -15,6 +17,47 @@ module Gitlab end end + class << self + def default_arguments_for(provider_name) + case provider_name + when 'cas3' + { on_single_sign_out: cas3_signout_handler } + when 'authentiq' + { remote_sign_out_handler: authentiq_signout_handler } + when 'shibboleth' + { fail_with_empty_uid: true } + when 'google_oauth2' + { client_options: { connection_opts: { request: { timeout: OAUTH2_TIMEOUT_SECONDS } } } } + else + {} + end + end + + private + + def cas3_signout_handler + lambda do |request| + ticket = request.params[:session_index] + raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket) + + Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket) + true + end + end + + def authentiq_signout_handler + lambda do |request| + authentiq_session = request.params['sid'] + if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session) + Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session) + true + else + false + end + end + end + end + private def add_provider_to_devise(*args) @@ -33,7 +76,8 @@ module Gitlab # An Array from the configuration will be expanded. provider_arguments.concat provider['args'] when Hash - hash_arguments = provider['args'].merge(provider_defaults(provider)) + defaults = provider_defaults(provider) + hash_arguments = provider['args'].deep_symbolize_keys.deep_merge(defaults) # A Hash from the configuration will be passed as is. provider_arguments << normalize_hash_arguments(hash_arguments) @@ -43,7 +87,7 @@ module Gitlab end def normalize_hash_arguments(args) - args.symbolize_keys! + args.deep_symbolize_keys! # Rails 5.1 deprecated the use of string names in the middleware # (https://github.com/rails/rails/commit/83b767ce), so we need to @@ -66,38 +110,7 @@ module Gitlab end def provider_defaults(provider) - case provider['name'] - when 'cas3' - { on_single_sign_out: cas3_signout_handler } - when 'authentiq' - { remote_sign_out_handler: authentiq_signout_handler } - when 'shibboleth' - { fail_with_empty_uid: true } - else - {} - end - end - - def cas3_signout_handler - lambda do |request| - ticket = request.params[:session_index] - raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket) - - Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket) - true - end - end - - def authentiq_signout_handler - lambda do |request| - authentiq_session = request.params['sid'] - if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session) - Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session) - true - else - false - end - end + self.class.default_arguments_for(provider['name']) end def omniauth_customized_providers diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb index 56f056fd869..319e5d2063c 100644 --- a/lib/google_api/auth.rb +++ b/lib/google_api/auth.rb @@ -37,6 +37,10 @@ module GoogleApi Gitlab::Auth::OAuth::Provider.config_for('google_oauth2') end + def client_options + config.args.client_options.deep_symbolize_keys + end + def client return @client if defined?(@client) @@ -49,7 +53,8 @@ module GoogleApi config.app_secret, site: 'https://accounts.google.com', token_url: '/o/oauth2/token', - authorize_url: '/o/oauth2/auth' + authorize_url: '/o/oauth2/auth', + **client_options ) end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3fc8557d9da..8a372f03487 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -829,6 +829,9 @@ msgstr "" msgid "<strong>%{group_name}</strong> group members" msgstr "" +msgid "<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>" +msgstr "" + msgid "<strong>Deletes</strong> source branch" msgstr "" @@ -877,7 +880,7 @@ msgstr "" msgid "A group is a collection of several projects" msgstr "" -msgid "A group represents your organization in GitLab." +msgid "A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects." msgstr "" msgid "A member of the abuse team will review your report as soon as possible." @@ -4577,6 +4580,9 @@ msgstr "" msgid "ClusterIntegration|Cluster name is required." msgstr "" +msgid "ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}" +msgstr "" + msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters." msgstr "" @@ -4757,6 +4763,9 @@ msgstr "" msgid "ClusterIntegration|Helm Tiller" msgstr "" +msgid "ClusterIntegration|Helm release failed to install" +msgstr "" + msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts." msgstr "" @@ -4919,6 +4928,9 @@ msgstr "" msgid "ClusterIntegration|No VPCs found" msgstr "" +msgid "ClusterIntegration|No deployment cluster found for this job" +msgstr "" + msgid "ClusterIntegration|No instance type found" msgstr "" @@ -5602,6 +5614,9 @@ msgstr "" msgid "ComplianceFramework|SOX - Sarbanes-Oxley" msgstr "" +msgid "ComplianceFramework|This project is regulated by %{framework}." +msgstr "" + msgid "Confidence: %{confidence}" msgstr "" @@ -5716,6 +5731,9 @@ msgstr "" msgid "Contact sales to upgrade" msgstr "" +msgid "Contact support" +msgstr "" + msgid "Container Registry" msgstr "" @@ -6243,9 +6261,6 @@ msgstr "" msgid "Create a Mattermost team for this group" msgstr "" -msgid "Create a group for your organization" -msgstr "" - msgid "Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies." msgstr "" @@ -6369,6 +6384,9 @@ msgstr "" msgid "Create your first page" msgstr "" +msgid "Create your group" +msgstr "" + msgid "CreateGroup|You don’t have permission to create a subgroup in this group." msgstr "" @@ -6920,6 +6938,12 @@ msgstr "" msgid "Delete domain" msgstr "" +msgid "Delete label" +msgstr "" + +msgid "Delete label: %{label_name} ?" +msgstr "" + msgid "Delete license" msgstr "" @@ -10667,7 +10691,7 @@ msgstr "" msgid "Group name" msgstr "" -msgid "Group name (Your organization)" +msgid "Group name (your organization)" msgstr "" msgid "Group overview" @@ -12729,6 +12753,9 @@ msgstr "" msgid "License|License" msgstr "" +msgid "License|Licensed user count exceeded" +msgstr "" + msgid "License|You can restore access to the Gold features at any time by upgrading." msgstr "" @@ -12744,6 +12771,9 @@ msgstr "" msgid "License|Your free trial of GitLab Ultimate expired on %{trial_ends_on}." msgstr "" +msgid "License|Your instance has exceeded your subscription's number of licensed users by %{extra_users_count}. You can continue to add more users and we'll include the overage in your next bill." +msgstr "" + msgid "Limit display of time tracking units to hours." msgstr "" @@ -21806,6 +21836,9 @@ msgstr "" msgid "This commit was signed with an <strong>unverified</strong> signature." msgstr "" +msgid "This content could not be displayed because %{reason}. You can %{options} instead." +msgstr "" + msgid "This date is after the due date, so this epic won't appear in the roadmap." msgstr "" @@ -22439,6 +22472,9 @@ msgstr "" msgid "Timeout" msgstr "" +msgid "Timeout connecting to the Google API. Please try again." +msgstr "" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "" @@ -25564,6 +25600,9 @@ msgstr "" msgid "done" msgstr "" +msgid "download it" +msgstr "" + msgid "draft" msgid_plural "drafts" msgstr[0] "" @@ -25768,6 +25807,12 @@ msgstr "" msgid "issues on track" msgstr "" +msgid "it is larger than %{limit}" +msgstr "" + +msgid "it is stored as a job artifact" +msgstr "" + msgid "it is stored externally" msgstr "" @@ -25807,6 +25852,9 @@ msgstr "" msgid "limit of %{project_limit} reached" msgstr "" +msgid "load it anyway" +msgstr "" + msgid "locked by %{path_lock_user_name} %{created_at}" msgstr "" @@ -26484,6 +26532,9 @@ msgstr "" msgid "view the blob" msgstr "" +msgid "view the source" +msgstr "" + msgid "vulnerability|Add a comment" msgstr "" diff --git a/package.json b/package.json index 5b5dddffc49..b43bda5f9d6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@gitlab/svgs": "1.127.0", "@gitlab/ui": "14.10.0", "@gitlab/visual-review-tools": "1.6.1", - "@rails/actioncable": "^6.0.2-2", + "@rails/actioncable": "^6.0.3", "@sentry/browser": "^5.10.2", "@sourcegraph/code-host-integration": "0.0.46", "@toast-ui/editor": "^2.0.1", diff --git a/scripts/clean-old-cached-assets b/scripts/clean-old-cached-assets index 9a373439e5e..20889b7ffe6 100755 --- a/scripts/clean-old-cached-assets +++ b/scripts/clean-old-cached-assets @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Clean up cached files that are older than 4 days find tmp/cache/assets/sprockets/ -type f -mtime +4 -execdir rm -- "{}" \; diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh index 8a049bcd7fb..4c9db68c9d3 100644 --- a/scripts/create_postgres_user.sh +++ b/scripts/create_postgres_user.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash psql -h postgres -U postgres postgres <<EOF CREATE USER gitlab; diff --git a/scripts/prepare_postgres_fdw.sh b/scripts/prepare_postgres_fdw.sh index 69442d2881b..246f3acc569 100755 --- a/scripts/prepare_postgres_fdw.sh +++ b/scripts/prepare_postgres_fdw.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash psql -h postgres -U postgres gitlabhq_geo_test <<EOF CREATE EXTENSION postgres_fdw; diff --git a/scripts/review_apps/gcp_cleanup.sh b/scripts/review_apps/gcp_cleanup.sh index efdde05a194..f289a50f629 100755 --- a/scripts/review_apps/gcp_cleanup.sh +++ b/scripts/review_apps/gcp_cleanup.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash source scripts/utils.sh diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index 622389f34b6..adaae0df870 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash function retrieve_tests_metadata() { mkdir -p knapsack/ rspec_flaky/ rspec_profiling/ diff --git a/scripts/security-harness b/scripts/security-harness index c101cd03454..b9492e16066 100755 --- a/scripts/security-harness +++ b/scripts/security-harness @@ -19,7 +19,7 @@ end HOOK_PATH = File.expand_path("../.git/hooks/pre-push", __dir__) HOOK_DATA = <<~HOOK - #!/bin/bash + #!/usr/bin/env bash set -e diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb index 58bda2bd4e8..9d0e0d92978 100644 --- a/spec/controllers/google_api/authorizations_controller_spec.rb +++ b/spec/controllers/google_api/authorizations_controller_spec.rb @@ -12,10 +12,6 @@ describe GoogleApi::AuthorizationsController do before do sign_in(user) - - allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| - allow(instance).to receive(:get_token).and_return([token, expires_at]) - end end shared_examples_for 'access denied' do @@ -38,6 +34,12 @@ describe GoogleApi::AuthorizationsController do context 'session key matches state param' do let(:state) { session_key } + before do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| + allow(instance).to receive(:get_token).and_return([token, expires_at]) + end + end + it 'sets token and expires_at in session' do subject @@ -63,6 +65,22 @@ describe GoogleApi::AuthorizationsController do it_behaves_like 'access denied' end + + context 'when a Faraday exception occurs' do + let(:state) { session_key } + + [::Faraday::TimeoutError, ::Faraday::ConnectionFailed].each do |error| + it "sets a flash alert on #{error}" do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| + allow(instance).to receive(:get_token).and_raise(error.new(nil)) + end + + subject + + expect(flash[:alert]).to eq('Timeout connecting to the Google API. Please try again.') + end + end + end end context 'state param is present, but session key is blank' do diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 61091514822..26c09795a0b 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -252,6 +252,21 @@ FactoryBot.define do end end + trait :cluster_applications do + file_type { :cluster_applications } + file_format { :gzip } + + transient do + file do + fixture_file_upload(Rails.root.join('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz'), 'application/x-gzip') + end + end + + after(:build) do |artifact, evaluator| + artifact.file = evaluator.file + end + end + trait :correct_checksum do after(:build) do |artifact, evaluator| artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 7e52c54d4f1..c49c26f06e5 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -65,6 +65,10 @@ FactoryBot.define do status_reason { 'something went wrong' } end + trait :uninstalled do + status { 10 } + end + trait :timed_out do installing updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz Binary files differnew file mode 100644 index 00000000000..bcbbba8dc00 --- /dev/null +++ b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz Binary files differnew file mode 100644 index 00000000000..0b39b42bdfa --- /dev/null +++ b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz Binary files differnew file mode 100644 index 00000000000..20cac36287b --- /dev/null +++ b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz diff --git a/spec/frontend/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js index c1ef08e0f1b..c4da7189751 100644 --- a/spec/frontend/avatar_helper_spec.js +++ b/spec/frontend/avatar_helper_spec.js @@ -15,10 +15,22 @@ function matchAll(str) { describe('avatar_helper', () => { describe('getIdenticonBackgroundClass', () => { - it('returns identicon bg class from id', () => { + it('returns identicon bg class from id that is a number', () => { expect(getIdenticonBackgroundClass(1)).toEqual('bg2'); }); + it('returns identicon bg class from id that is a string', () => { + expect(getIdenticonBackgroundClass('1')).toEqual('bg2'); + }); + + it('returns identicon bg class from id that is a GraphQL string id', () => { + expect(getIdenticonBackgroundClass('gid://gitlab/Project/1')).toEqual('bg2'); + }); + + it('returns identicon bg class from unparsable string', () => { + expect(getIdenticonBackgroundClass('gid://gitlab/')).toEqual('bg1'); + }); + it(`wraps around if id is bigger than ${IDENTICON_BG_COUNT}`, () => { expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT + 4)).toEqual('bg5'); expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT * 5 + 6)).toEqual('bg7'); diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js index 58a9ee761df..6eb5cfb71aa 100644 --- a/spec/frontend/blob/components/blob_content_error_spec.js +++ b/spec/frontend/blob/components/blob_content_error_spec.js @@ -1,27 +1,60 @@ import { shallowMount } from '@vue/test-utils'; import BlobContentError from '~/blob/components/blob_content_error.vue'; +import { GlSprintf } from '@gitlab/ui'; + +import { BLOB_RENDER_ERRORS } from '~/blob/components/constants'; describe('Blob Content Error component', () => { let wrapper; - const viewerError = '<h1 id="error">Foo Error</h1>'; - function createComponent() { + function createComponent(props = {}) { wrapper = shallowMount(BlobContentError, { propsData: { - viewerError, + ...props, + }, + stubs: { + GlSprintf, }, }); } - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); - it('renders the passed error without transformations', () => { - expect(wrapper.html()).toContain(viewerError); + describe('collapsed and too large blobs', () => { + it.each` + error | reason | options + ${BLOB_RENDER_ERRORS.REASONS.COLLAPSED} | ${'it is larger than 1.00 MiB'} | ${[BLOB_RENDER_ERRORS.OPTIONS.LOAD.text, BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]} + ${BLOB_RENDER_ERRORS.REASONS.TOO_LARGE} | ${'it is larger than 100.00 MiB'} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]} + `('renders correct reason for $error.id', ({ error, reason, options }) => { + createComponent({ + viewerError: error.id, + }); + expect(wrapper.text()).toContain(reason); + options.forEach(option => { + expect(wrapper.text()).toContain(option); + }); + }); + }); + + describe('external blob', () => { + it.each` + storageType | reason | options + ${'lfs'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.lfs} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]} + ${'build_artifact'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.build_artifact} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]} + ${'default'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.default} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]} + `('renders correct reason for $storageType blob', ({ storageType, reason, options }) => { + createComponent({ + viewerError: BLOB_RENDER_ERRORS.REASONS.EXTERNAL.id, + blob: { + externalStorage: storageType, + }, + }); + expect(wrapper.text()).toContain(reason); + options.forEach(option => { + expect(wrapper.text()).toContain(option); + }); + }); }); }); diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js index ff153007be9..244ed41869d 100644 --- a/spec/frontend/blob/components/blob_content_spec.js +++ b/spec/frontend/blob/components/blob_content_spec.js @@ -2,6 +2,12 @@ import { shallowMount } from '@vue/test-utils'; import BlobContent from '~/blob/components/blob_content.vue'; import BlobContentError from '~/blob/components/blob_content_error.vue'; import { + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, + BLOB_RENDER_ERRORS, +} from '~/blob/components/constants'; +import { + Blob, RichViewerMock, SimpleViewerMock, RichBlobContentMock, @@ -67,4 +73,32 @@ describe('Blob Content component', () => { expect(wrapper.find(viewer).html()).toContain(content); }); }); + + describe('functionality', () => { + describe('render error', () => { + const findErrorEl = () => wrapper.find(BlobContentError); + const renderError = BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id; + const viewer = { ...SimpleViewerMock, renderError }; + + beforeEach(() => { + createComponent({ blob: Blob }, viewer); + }); + + it('correctly sets blob on the blob-content-error component', () => { + expect(findErrorEl().props('blob')).toEqual(Blob); + }); + + it(`properly proxies ${BLOB_RENDER_EVENT_LOAD} event`, () => { + expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeUndefined(); + findErrorEl().vm.$emit(BLOB_RENDER_EVENT_LOAD); + expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeTruthy(); + }); + + it(`properly proxies ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => { + expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeUndefined(); + findErrorEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE); + expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeTruthy(); + }); + }); + }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index 612ca858f05..d06489cffa9 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -3,6 +3,7 @@ import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; import BlobHeader from '~/blob/components/blob_header.vue'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import BlobContent from '~/blob/components/blob_content.vue'; +import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from '~/blob/components/constants'; import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; import { SNIPPET_VISIBILITY_PRIVATE, @@ -29,6 +30,8 @@ describe('Blob Embeddable', () => { queries: { blobContent: { loading: contentLoading, + refetch: jest.fn(), + skip: true, }, }, }; @@ -143,4 +146,35 @@ describe('Blob Embeddable', () => { }); }); }); + + describe('functionality', () => { + describe('render error', () => { + const findContentEl = () => wrapper.find(BlobContent); + + it('correctly sets blob on the blob-content-error component', () => { + createComponent(); + expect(findContentEl().props('blob')).toEqual(BlobMock); + }); + + it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, () => { + createComponent(); + + expect(wrapper.vm.$apollo.queries.blobContent.refetch).not.toHaveBeenCalled(); + findContentEl().vm.$emit(BLOB_RENDER_EVENT_LOAD); + expect(wrapper.vm.$apollo.queries.blobContent.refetch).toHaveBeenCalledTimes(1); + }); + + it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => { + createComponent( + {}, + { + activeViewerType: RichViewerMock.type, + }, + ); + + findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE); + expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap index 72370cb5b52..1d8e04b83a3 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap @@ -1,6 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Identicon matches snapshot 1`] = ` +exports[`Identicon entity id is a GraphQL id matches snapshot 1`] = ` +<div + class="avatar identicon s40 bg2" +> + + E + +</div> +`; + +exports[`Identicon entity id is a number matches snapshot 1`] = ` <div class="avatar identicon s40 bg2" > diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js index 5e8b013d480..53a55dcd6bd 100644 --- a/spec/frontend/vue_shared/components/identicon_spec.js +++ b/spec/frontend/vue_shared/components/identicon_spec.js @@ -4,12 +4,17 @@ import IdenticonComponent from '~/vue_shared/components/identicon.vue'; describe('Identicon', () => { let wrapper; - const createComponent = () => { + const defaultProps = { + entityId: 1, + entityName: 'entity-name', + sizeClass: 's40', + }; + + const createComponent = (props = {}) => { wrapper = shallowMount(IdenticonComponent, { propsData: { - entityId: 1, - entityName: 'entity-name', - sizeClass: 's40', + ...defaultProps, + ...props, }, }); }; @@ -19,15 +24,27 @@ describe('Identicon', () => { wrapper = null; }); - it('matches snapshot', () => { - createComponent(); + describe('entity id is a number', () => { + beforeEach(createComponent); + + it('matches snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); - expect(wrapper.element).toMatchSnapshot(); + it('adds a correct class to identicon', () => { + expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2'); + }); }); - it('adds a correct class to identicon', () => { - createComponent(); + describe('entity id is a GraphQL id', () => { + beforeEach(() => createComponent({ entityId: 'gid://gitlab/Project/8' })); + + it('matches snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); - expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2'); + it('adds a correct class to identicon', () => { + expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2'); + }); }); }); diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index d679768be6c..05231cc6d09 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -71,6 +71,28 @@ describe ApplicationHelper do end end + describe '#admin_section?' do + context 'when controller is under the admin namespace' do + before do + allow(helper).to receive(:controller).and_return(Admin::UsersController.new) + end + + it 'returns true' do + expect(helper.admin_section?).to eq(true) + end + end + + context 'when controller is not under the admin namespace' do + before do + allow(helper).to receive(:controller).and_return(UsersController.new) + end + + it 'returns true' do + expect(helper.admin_section?).to eq(false) + end + end + end + describe 'simple_sanitize' do let(:a_tag) { '<a href="#">Foo</a>' } diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb index f46f9d76a1e..8b0d4d786cd 100644 --- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb @@ -63,7 +63,7 @@ describe Gitlab::Auth::OAuth::Provider do context 'for an OmniAuth provider' do before do provider = OpenStruct.new( - name: 'google', + name: 'google_oauth2', app_id: 'asd123', app_secret: 'asd123' ) @@ -71,8 +71,16 @@ describe Gitlab::Auth::OAuth::Provider do end context 'when the provider exists' do + subject { described_class.config_for('google_oauth2') } + it 'returns the config' do - expect(described_class.config_for('google')).to be_a(OpenStruct) + expect(subject).to be_a(OpenStruct) + end + + it 'merges defaults with the given configuration' do + defaults = Gitlab::OmniauthInitializer.default_arguments_for('google_oauth2').deep_stringify_keys + + expect(subject['args']).to include(defaults) end end diff --git a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb index ea1fbc98c9f..0ad5dc189c0 100644 --- a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb @@ -82,5 +82,19 @@ describe Gitlab::Kubernetes::Helm::Parsers::ListV2 do expect(list_v2.releases).to eq([]) end end + + context 'invalid Releases' do + let(:invalid_file_contents) do + '{ "Releases" : ["a", "b"] }' + end + + subject(:list_v2) { described_class.new(invalid_file_contents) } + + it 'raises an error' do + expect do + list_v2.releases + end.to raise_error(described_class::ParserError, 'Invalid format for Releases') + end + end end end diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index 99684bb2ab2..4afe4545891 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -86,6 +86,22 @@ describe Gitlab::OmniauthInitializer do subject.execute([cas3_config]) end + it 'configures defaults for google_oauth2' do + google_config = { + 'name' => 'google_oauth2', + "args" => { "access_type" => "offline", "approval_prompt" => '' } + } + + expect(devise_config).to receive(:omniauth).with( + :google_oauth2, + access_type: "offline", + approval_prompt: "", + client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } } + ) + + subject.execute([google_config]) + end + it 'converts client_auth_method to a Symbol for openid_connect' do openid_connect_config = { 'name' => 'openid_connect', diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb index 719e98c5fdf..fa4e6288681 100644 --- a/spec/lib/google_api/auth_spec.rb +++ b/spec/lib/google_api/auth_spec.rb @@ -40,5 +40,19 @@ describe GoogleApi::Auth do expect(token).to eq('token') expect(expires_at).to eq('expires_at') end + + it 'expects the client to receive default options' do + config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2') + + expect(OAuth2::Client).to receive(:new).with( + config.app_id, + config.app_secret, + hash_including( + **config.args.client_options.deep_symbolize_keys + ) + ).and_call_original + + client.get_token('xxx') + end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 5b7d69677bf..8273aaeae68 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -590,6 +590,60 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do end end + describe '#find_or_build_application' do + let_it_be(:cluster, reload: true) { create(:cluster) } + + it 'rejects classes that are not applications' do + expect do + cluster.find_or_build_application(Project) + end.to raise_error(ArgumentError) + end + + context 'when none of applications are created' do + it 'returns the new application', :aggregate_failures do + described_class::APPLICATIONS.values.each do |application_class| + application = cluster.find_or_build_application(application_class) + + expect(application).to be_a(application_class) + expect(application).not_to be_persisted + end + end + end + + context 'when application is persisted' do + let!(:helm) { create(:clusters_applications_helm, cluster: cluster) } + let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) } + let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) } + let!(:crossplane) { create(:clusters_applications_crossplane, cluster: cluster) } + let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } + let!(:runner) { create(:clusters_applications_runner, cluster: cluster) } + let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) } + let!(:knative) { create(:clusters_applications_knative, cluster: cluster) } + let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) } + let!(:fluentd) { create(:clusters_applications_fluentd, cluster: cluster) } + + it 'returns the persisted application', :aggregate_failures do + { + Clusters::Applications::Helm => helm, + Clusters::Applications::Ingress => ingress, + Clusters::Applications::CertManager => cert_manager, + Clusters::Applications::Crossplane => crossplane, + Clusters::Applications::Prometheus => prometheus, + Clusters::Applications::Runner => runner, + Clusters::Applications::Jupyter => jupyter, + Clusters::Applications::Knative => knative, + Clusters::Applications::ElasticStack => elastic_stack, + Clusters::Applications::Fluentd => fluentd + }.each do |application_class, expected_object| + application = cluster.find_or_build_application(application_class) + + expect(application).to eq(expected_object) + expect(application).to be_persisted + end + end + end + end + describe '#allow_user_defined_namespace?' do subject { cluster.allow_user_defined_namespace? } diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 73acbd1ea41..097171bbf8e 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -224,6 +224,20 @@ describe API::ProjectSnippets do expect(response).to have_gitlab_http_status(:bad_request) end + context 'when save fails because the repository could not be created' do + before do + allow_next_instance_of(Snippets::CreateService) do |instance| + allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError) + end + end + + it 'returns 400' do + post api("/projects/#{project.id}/snippets", admin), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'when the snippet is spam' do def create_snippet(project, snippet_params = {}) project.add_developer(user) diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 8003b8f4b46..074e662adcd 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -267,6 +267,28 @@ describe API::Snippets do expect(response).to have_gitlab_http_status(:bad_request) end + it 'returns 400 for validation errors' do + params[:title] = '' + + post api("/snippets/", user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'when save fails because the repository could not be created' do + before do + allow_next_instance_of(Snippets::CreateService) do |instance| + allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError) + end + end + + it 'returns 400' do + post api("/snippets/", user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'when the snippet is spam' do def create_snippet(snippet_params = {}) post api('/snippets', user), params: params.merge(snippet_params) @@ -356,6 +378,12 @@ describe API::Snippets do expect(response).to have_gitlab_http_status(:bad_request) end + it 'returns 400 for validation errors' do + update_snippet(params: { title: '' }) + + expect(response).to have_gitlab_http_status(:bad_request) + end + it_behaves_like 'update with repository actions' do let(:snippet_without_repo) { create(:personal_snippet, author: user, visibility_level: visibility_level) } end diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb index 2d869575d2a..4d49923a184 100644 --- a/spec/services/ci/create_job_artifacts_service_spec.rb +++ b/spec/services/ci/create_job_artifacts_service_spec.rb @@ -177,6 +177,53 @@ describe Ci::CreateJobArtifactsService do end end + context 'when artifact type is cluster_applications' do + let(:artifacts_file) do + file_to_upload('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz', sha256: artifacts_sha256) + end + + let(:params) do + { + 'artifact_type' => 'cluster_applications', + 'artifact_format' => 'gzip' + } + end + + it 'calls cluster applications parse service' do + expect_next_instance_of(Clusters::ParseClusterApplicationsArtifactService) do |service| + expect(service).to receive(:execute).once.and_call_original + end + + subject + end + + context 'when there is a deployment cluster' do + let(:user) { project.owner } + + before do + job.update!(user: user) + end + + it 'calls cluster applications parse service with job and job user', :aggregate_failures do + expect(Clusters::ParseClusterApplicationsArtifactService).to receive(:new).with(job, user).and_call_original + + subject + end + end + + context 'when ci_synchronous_artifact_parsing feature flag is disabled' do + before do + stub_feature_flags(ci_synchronous_artifact_parsing: false) + end + + it 'does not call parse service' do + expect(Clusters::ParseClusterApplicationsArtifactService).not_to receive(:new) + + expect(subject[:status]).to eq(:success) + end + end + end + shared_examples 'rescues object storage error' do |klass, message, expected_message| it "handles #{klass}" do allow_next_instance_of(JobArtifactUploader) do |uploader| diff --git a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb new file mode 100644 index 00000000000..f14c929554a --- /dev/null +++ b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::ParseClusterApplicationsArtifactService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + before do + project.add_maintainer(user) + end + + describe 'RELEASE_NAMES' do + it 'is included in Cluster application names', :aggregate_failures do + described_class::RELEASE_NAMES.each do |release_name| + expect(Clusters::Cluster::APPLICATIONS).to include(release_name) + end + end + end + + describe '.new' do + let(:job) { build(:ci_build) } + + it 'sets the project and current user', :aggregate_failures do + service = described_class.new(job, user) + + expect(service.project).to eq(job.project) + expect(service.current_user).to eq(user) + end + end + + describe '#execute' do + let_it_be(:cluster, reload: true) { create(:cluster, projects: [project]) } + let_it_be(:deployment, reload: true) { create(:deployment, cluster: cluster) } + + let(:job) { deployment.deployable } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job) } + + context 'when cluster_applications_artifact feature flag is disabled' do + before do + stub_feature_flags(cluster_applications_artifact: false) + end + + it 'does not call Gitlab::Kubernetes::Helm::Parsers::ListV2 and returns success immediately' do + expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).not_to receive(:new) + + result = described_class.new(job, user).execute(artifact) + + expect(result[:status]).to eq(:success) + end + end + + context 'when cluster_applications_artifact feature flag is enabled for project' do + before do + stub_feature_flags(cluster_applications_artifact: job.project) + end + + it 'calls Gitlab::Kubernetes::Helm::Parsers::ListV2' do + expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).to receive(:new).and_call_original + + result = described_class.new(job, user).execute(artifact) + + expect(result[:status]).to eq(:success) + end + + context 'artifact is not of cluster_applications type' do + let(:artifact) { create(:ci_job_artifact, :archive) } + let(:job) { artifact.job } + + it 'raise ArgumentError' do + expect do + described_class.new(job, user).execute(artifact) + end.to raise_error(ArgumentError, 'Artifact is not cluster_applications file type') + end + end + + context 'artifact exceeds acceptable size' do + it 'returns an error' do + stub_const("#{described_class}::MAX_ACCEPTABLE_ARTIFACT_SIZE", 1.byte) + + result = described_class.new(job, user).execute(artifact) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Cluster_applications artifact too big. Maximum allowable size: 1 Byte') + end + end + + context 'job has no deployment cluster' do + let(:job) { build(:ci_build) } + + it 'returns an error' do + result = described_class.new(job, user).execute(artifact) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('No deployment cluster found for this job') + end + end + + context 'job has deployment cluster' do + context 'current user does not have access to deployment cluster' do + let(:other_user) { create(:user) } + + it 'returns an error' do + result = described_class.new(job, other_user).execute(artifact) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('No deployment cluster found for this job') + end + end + + context 'release is missing' do + let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz' } + let(:file) { fixture_file_upload(Rails.root.join(fixture)) } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) } + + context 'application does not exist' do + it 'does not create or destroy an application' do + expect do + described_class.new(job, user).execute(artifact) + end.not_to change(Clusters::Applications::Prometheus, :count) + end + end + + context 'application exists' do + before do + create(:clusters_applications_prometheus, :installed, cluster: cluster) + end + + it 'marks the application as uninstalled' do + described_class.new(job, user).execute(artifact) + + cluster.application_prometheus.reload + expect(cluster.application_prometheus).to be_uninstalled + end + end + end + + context 'release is deployed' do + let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz' } + let(:file) { fixture_file_upload(Rails.root.join(fixture)) } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) } + + context 'application does not exist' do + it 'creates an application and marks it as installed' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(Clusters::Applications::Prometheus, :count) + + expect(cluster.application_prometheus).to be_persisted + expect(cluster.application_prometheus).to be_installed + end + end + + context 'application exists' do + before do + create(:clusters_applications_prometheus, :errored, cluster: cluster) + end + + it 'marks the application as installed' do + described_class.new(job, user).execute(artifact) + + expect(cluster.application_prometheus).to be_installed + end + end + end + + context 'release is failed' do + let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz' } + let(:file) { fixture_file_upload(Rails.root.join(fixture)) } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) } + + context 'application does not exist' do + it 'creates an application and marks it as errored' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(Clusters::Applications::Prometheus, :count) + + expect(cluster.application_prometheus).to be_persisted + expect(cluster.application_prometheus).to be_errored + expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install') + end + end + + context 'application exists' do + before do + create(:clusters_applications_prometheus, :installed, cluster: cluster) + end + + it 'marks the application as errored' do + described_class.new(job, user).execute(artifact) + + expect(cluster.application_prometheus).to be_errored + expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install') + end + end + end + end + end + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 37f1b33d455..c2fd04d648b 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -194,6 +194,66 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end + describe '#make_externally_installed' do + subject { create(application_name, :installing) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + + context 'application is updated' do + subject { create(application_name, :updated) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + end + + context 'application is errored' do + subject { create(application_name, :errored) } + + it 'is installed' do + subject.make_externally_installed + + expect(subject).to be_installed + end + end + end + + describe '#make_externally_uninstalled' do + subject { create(application_name, :installed) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + + context 'application is updated' do + subject { create(application_name, :updated) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + end + + context 'application is errored' do + subject { create(application_name, :errored) } + + it 'is uninstalled' do + subject.make_externally_uninstalled + + expect(subject).to be_uninstalled + end + end + end + describe '#make_scheduled' do subject { create(application_name, :installable) } @@ -278,6 +338,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| :update_errored | false :uninstalling | false :uninstall_errored | false + :uninstalled | false :timed_out | false end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index dd13a9c166e..f830f957174 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -48,6 +48,28 @@ RSpec.shared_examples 'update with repository actions' do expect(blob).not_to be_nil expect(blob.data).to eq content end + + context 'when save fails due to a repository creation error' do + let(:content) { 'File content' } + let(:file_name) { 'test.md' } + + before do + allow_next_instance_of(Snippets::UpdateService) do |instance| + allow(instance).to receive(:create_repository_for).with(snippet).and_raise(Snippets::UpdateService::CreateRepositoryError) + end + + update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name }) + end + + it 'returns 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'does not save the changes to the snippet object' do + expect(snippet.content).not_to eq(content) + expect(snippet.file_name).not_to eq(file_name) + end + end end end end diff --git a/vendor/elastic_stack/wait-for-elasticsearch.sh b/vendor/elastic_stack/wait-for-elasticsearch.sh index 1423af2e10b..33c5eaae9ef 100755 --- a/vendor/elastic_stack/wait-for-elasticsearch.sh +++ b/vendor/elastic_stack/wait-for-elasticsearch.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # http://redsymbol.net/articles/unofficial-bash-strict-mode/ IFS=$'\n\t' set -euo pipefail diff --git a/yarn.lock b/yarn.lock index 25789a2d4fc..e38111fa438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -983,10 +983,10 @@ consola "^2.10.1" node-fetch "^2.6.0" -"@rails/actioncable@^6.0.2-2": - version "6.0.2-2" - resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.2-2.tgz#237907f8111707950381387c273b19ac25958408" - integrity sha512-0sKStf8hnberH1TKup10PJ92JT2dVqf3gf+OT4lJ7DiYSBEuDcvICHxWsyML2oWTpjUhC4kLvUJ3pXL2JJrJuQ== +"@rails/actioncable@^6.0.3": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3.tgz#722b4b639936129307ddbab3a390f6bcacf3e7bc" + integrity sha512-I01hgqxxnOgOtJTGlq0ZsGJYiTEEiSGVEGQn3vimZSqEP1HqzyFNbzGTq14Xdyeow2yGJjygjoFF1pmtE+SQaw== "@sentry/browser@^5.10.2": version "5.10.2" |