summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/CODEOWNERS3
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue14
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue17
-rw-r--r--app/assets/javascripts/blob/components/blob_content_error.vue71
-rw-r--r--app/assets/javascripts/blob/components/constants.js56
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js5
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/persistent_user_callout.js5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue34
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/controllers/google_api/authorizations_controller.rb3
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/models/clusters/cluster.rb12
-rw-r--r--app/models/clusters/concerns/application_status.rb9
-rw-r--r--app/services/ci/create_job_artifacts_service.rb5
-rw-r--r--app/services/clusters/parse_cluster_applications_artifact_service.rb95
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/shared/_delete_label_modal.html.haml11
-rwxr-xr-xbin/background_jobs2
-rwxr-xr-xbin/background_jobs_sk2
-rwxr-xr-xbin/background_jobs_sk_cluster2
-rw-r--r--changelogs/unreleased/216453-large-files-snippet.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml5
-rw-r--r--changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml5
-rw-r--r--changelogs/unreleased/dblessing-google-oauth2-timeout.yml5
-rw-r--r--changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml5
-rw-r--r--changelogs/unreleased/tr-fix-space-char.yml5
-rw-r--r--config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb17
-rw-r--r--doc/administration/packages/container_registry.md21
-rw-r--r--doc/ci/docker/using_kaniko.md1
-rw-r--r--doc/ci/yaml/README.md6
-rw-r--r--doc/development/code_review.md17
-rw-r--r--doc/raketasks/x509_signatures.md6
-rw-r--r--doc/user/group/epics/manage_epics.md8
-rw-r--r--lib/api/project_snippets.rb16
-rw-r--r--lib/api/snippets.rb16
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb14
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml34
-rw-r--r--lib/gitlab/danger/commit_linter.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/parsers/list_v2.rb12
-rw-r--r--lib/gitlab/omniauth_initializer.rb81
-rw-r--r--lib/google_api/auth.rb7
-rw-r--r--locale/gitlab.pot61
-rw-r--r--package.json2
-rwxr-xr-xscripts/clean-old-cached-assets2
-rw-r--r--scripts/create_postgres_user.sh2
-rwxr-xr-xscripts/prepare_postgres_fdw.sh2
-rwxr-xr-xscripts/review_apps/gcp_cleanup.sh2
-rw-r--r--scripts/rspec_helpers.sh2
-rwxr-xr-xscripts/security-harness2
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb26
-rw-r--r--spec/factories/ci/job_artifacts.rb15
-rw-r--r--spec/factories/clusters/applications/helm.rb4
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gzbin0 -> 338 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gzbin0 -> 339 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gzbin0 -> 320 bytes
-rw-r--r--spec/frontend/avatar_helper_spec.js14
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js51
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js34
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js37
-rw-r--r--spec/helpers/application_helper_spec.rb22
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb12
-rw-r--r--spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb14
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb16
-rw-r--r--spec/lib/google_api/auth_spec.rb14
-rw-r--r--spec/models/clusters/cluster_spec.rb54
-rw-r--r--spec/requests/api/project_snippets_spec.rb14
-rw-r--r--spec/requests/api/snippets_spec.rb28
-rw-r--r--spec/services/ci/create_job_artifacts_service_spec.rb47
-rw-r--r--spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb200
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb22
-rwxr-xr-xvendor/elastic_stack/wait-for-elasticsearch.sh2
-rw-r--r--yarn.lock8
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">&bull;</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 } &times;
.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
new file mode 100644
index 00000000000..bcbbba8dc00
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
new file mode 100644
index 00000000000..0b39b42bdfa
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
new file mode 100644
index 00000000000..20cac36287b
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
Binary files differ
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"