Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-18 12:08:08 +00:00
parent 1b9a2ce278
commit 48650fe1bf
77 changed files with 1302 additions and 160 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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,
},
},
};

View File

@ -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}`;
}

View File

@ -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');

View File

@ -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) {

View File

@ -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>

View File

@ -17,6 +17,8 @@ fragment SnippetBase on Snippet {
path
rawPath
size
externalStorage
renderedAsText
simpleViewer {
...BlobViewer
}

View File

@ -4,7 +4,7 @@ import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar
export default {
props: {
entityId: {
type: Number,
type: [Number, String],
required: true,
},
entityName: {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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'

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd $(dirname $0)/..

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd $(dirname $0)/..
app_root=$(pwd)

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd $(dirname $0)/..
app_root=$(pwd)

View File

@ -0,0 +1,5 @@
---
title: Refactored render errors for blob to Vue
merge_request: 32345
author:
type: changed

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Improve responses in the snippet create/update API endpoints
merge_request: 32282
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Set timeout for Google OAuth to prevent 503 error
merge_request: 30653
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add nested file detection for Dependency Scanning
merge_request: 31932
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix missing space character in alert header
merge_request: 32395
author:
type: fixed

View File

@ -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

View File

@ -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.

View File

@ -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-----

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}'

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 dont 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 ""

View File

@ -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",

View File

@ -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 -- "{}" \;

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
psql -h postgres -U postgres postgres <<EOF
CREATE USER gitlab;

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
psql -h postgres -U postgres gitlabhq_geo_test <<EOF
CREATE EXTENSION postgres_fdw;

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
source scripts/utils.sh

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
function retrieve_tests_metadata() {
mkdir -p knapsack/ rspec_flaky/ rspec_profiling/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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');

View File

@ -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);
});
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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"
>

View File

@ -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);
expect(wrapper.element).toMatchSnapshot();
it('matches snapshot', () => {
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' }));
expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
it('matches snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('adds a correct class to identicon', () => {
expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
});
});
});

View File

@ -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>' }

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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? }

View File

@ -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)

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
IFS=$'\n\t'
set -euo pipefail

View File

@ -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"