Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ecf2b5b604
commit
43c14d2d92
|
@ -137,7 +137,7 @@ Dangerfile @gl-quality/eng-prod
|
|||
/app/assets/javascripts/notes @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/merge_conflicts @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/mr_notes @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/mr_popover @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/issuable/popover @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/vue_merge_request_widget @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/merge_request.js @viktomas @jboyson @iamphill @thomasrandolph
|
||||
/app/assets/javascripts/merge_request_tabs.js @viktomas @jboyson @iamphill @thomasrandolph
|
||||
|
|
|
@ -24,11 +24,11 @@ $.fn.renderGFM = function renderGFM() {
|
|||
highlightCurrentUser(this.find('.gfm-project_member').get());
|
||||
initUserPopovers(this.find('.js-user-link').get());
|
||||
|
||||
const mrPopoverElements = this.find('.gfm-merge_request').get();
|
||||
if (mrPopoverElements.length) {
|
||||
import(/* webpackChunkName: 'MrPopoverBundle' */ '~/mr_popover')
|
||||
.then(({ default: initMRPopovers }) => {
|
||||
initMRPopovers(mrPopoverElements);
|
||||
const issuablePopoverElements = this.find('.gfm-merge_request').get();
|
||||
if (issuablePopoverElements.length) {
|
||||
import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
|
||||
.then(({ default: initIssuablePopovers }) => {
|
||||
initIssuablePopovers(issuablePopoverElements);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ export default {
|
|||
v-gl-tooltip.bottom
|
||||
class="gl-ml-3"
|
||||
:is-deleting="isDeleting"
|
||||
button-variant="warning"
|
||||
button-variant="default"
|
||||
button-icon="archive"
|
||||
button-category="secondary"
|
||||
:title="s__('DesignManagement|Archive design')"
|
||||
|
|
|
@ -6,8 +6,8 @@ import MRPopover from './components/mr_popover.vue';
|
|||
let renderedPopover;
|
||||
let renderFn;
|
||||
|
||||
const handleUserPopoverMouseOut = ({ target }) => {
|
||||
target.removeEventListener('mouseleave', handleUserPopoverMouseOut);
|
||||
const handleIssuablePopoverMouseOut = ({ target }) => {
|
||||
target.removeEventListener('mouseleave', handleIssuablePopoverMouseOut);
|
||||
|
||||
if (renderFn) {
|
||||
clearTimeout(renderFn);
|
||||
|
@ -22,9 +22,11 @@ const handleUserPopoverMouseOut = ({ target }) => {
|
|||
* Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes.
|
||||
* loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover
|
||||
*/
|
||||
const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) => ({ target }) => {
|
||||
const handleIssuablePopoverMount = ({ apolloProvider, projectPath, title, iid }) => ({
|
||||
target,
|
||||
}) => {
|
||||
// Add listener to actually remove it again
|
||||
target.addEventListener('mouseleave', handleUserPopoverMouseOut);
|
||||
target.addEventListener('mouseleave', handleIssuablePopoverMouseOut);
|
||||
|
||||
renderFn = setTimeout(() => {
|
||||
const MRPopoverComponent = Vue.extend(MRPopover);
|
||||
|
@ -33,7 +35,7 @@ const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) =>
|
|||
target,
|
||||
projectPath,
|
||||
mergeRequestIID: iid,
|
||||
mergeRequestTitle: mrTitle,
|
||||
mergeRequestTitle: title,
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
|
@ -43,22 +45,22 @@ const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) =>
|
|||
};
|
||||
|
||||
export default (elements) => {
|
||||
const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')];
|
||||
if (mrLinks.length > 0) {
|
||||
if (elements.length > 0) {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
const listenerAddedAttr = 'data-mr-listener-added';
|
||||
const listenerAddedAttr = 'data-popover-listener-added';
|
||||
|
||||
mrLinks.forEach((el) => {
|
||||
const { projectPath, mrTitle, iid } = el.dataset;
|
||||
elements.forEach((el) => {
|
||||
const { projectPath, iid } = el.dataset;
|
||||
const title = el.dataset.mrTitle || el.title;
|
||||
|
||||
if (!el.getAttribute(listenerAddedAttr) && projectPath && mrTitle && iid) {
|
||||
if (!el.getAttribute(listenerAddedAttr) && projectPath && title && iid) {
|
||||
el.addEventListener(
|
||||
'mouseenter',
|
||||
handleMRPopoverMount({ apolloProvider, projectPath, mrTitle, iid }),
|
||||
handleIssuablePopoverMount({ apolloProvider, projectPath, title, iid }),
|
||||
);
|
||||
el.setAttribute(listenerAddedAttr, true);
|
||||
}
|
|
@ -294,14 +294,20 @@ export default {
|
|||
/>
|
||||
<emoji-picker
|
||||
v-if="canAwardEmoji"
|
||||
toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
|
||||
toggle-class="note-action-button note-emoji-button btn-icon gl-shadow-none!"
|
||||
data-testid="note-emoji-button"
|
||||
@click="setAwardEmoji"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
|
||||
<gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
|
||||
<gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
|
||||
<gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" />
|
||||
<gl-icon
|
||||
class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!"
|
||||
name="smiley"
|
||||
/>
|
||||
<gl-icon
|
||||
class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!"
|
||||
name="smile"
|
||||
/>
|
||||
</template>
|
||||
</emoji-picker>
|
||||
<reply-button
|
||||
|
|
|
@ -26,9 +26,9 @@ import {
|
|||
import $ from 'jquery';
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import initMRPopovers from '~/mr_popover/';
|
||||
import noteHeader from '~/notes/components/note_header.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { spriteIcon } from '~/lib/utils/common_utils';
|
||||
|
@ -94,7 +94,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
|
||||
$(this.$refs['gfm-content']).renderGFM();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
|
||||
|
@ -130,7 +130,7 @@ export default {
|
|||
<div class="timeline-content">
|
||||
<div class="note-header">
|
||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||
<span v-safe-html="actionTextHtml"></span>
|
||||
<span ref="gfm-content" v-safe-html="actionTextHtml"></span>
|
||||
<template
|
||||
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
|
||||
#extra-controls
|
||||
|
|
|
@ -6,6 +6,7 @@ import workItemTitleSubscription from '../graphql/work_item_title.subscription.g
|
|||
import WorkItemActions from './work_item_actions.vue';
|
||||
import WorkItemState from './work_item_state.vue';
|
||||
import WorkItemTitle from './work_item_title.vue';
|
||||
import WorkItemLinks from './work_item_links/work_item_links.vue';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
@ -15,6 +16,7 @@ export default {
|
|||
WorkItemActions,
|
||||
WorkItemTitle,
|
||||
WorkItemState,
|
||||
WorkItemLinks,
|
||||
},
|
||||
props: {
|
||||
workItemId: {
|
||||
|
@ -105,6 +107,7 @@ export default {
|
|||
@error="error = $event"
|
||||
@updated="$emit('workItemUpdated')"
|
||||
/>
|
||||
<work-item-links :work-item-id="workItem.id" />
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import WorkItemLinksForm from './work_item_links_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
WorkItemLinksForm,
|
||||
},
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShownAddForm: false,
|
||||
isOpen: false,
|
||||
children: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// Only used for children for now but should be extended later to support parents and siblings
|
||||
isChildrenEmpty() {
|
||||
return this.children.length === 0;
|
||||
},
|
||||
toggleIcon() {
|
||||
return this.isOpen ? 'angle-up' : 'angle-down';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
},
|
||||
toggleAddForm() {
|
||||
this.isShownAddForm = !this.isShownAddForm;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
title: s__('WorkItem|Child items'),
|
||||
emptyStateMessage: s__(
|
||||
'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!',
|
||||
),
|
||||
addChildButtonLabel: s__('WorkItem|Add a child'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100">
|
||||
<div
|
||||
class="gl-p-4 gl-display-flex gl-justify-content-space-between"
|
||||
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
|
||||
>
|
||||
<h5 class="gl-m-0 gl-line-height-32">{{ $options.i18n.title }}</h5>
|
||||
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-4">
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
:icon="toggleIcon"
|
||||
data-testid="toggle-links"
|
||||
@click="toggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isOpen" class="gl-bg-gray-10 gl-p-4" data-testid="links-body">
|
||||
<div v-if="isChildrenEmpty" class="gl-px-8" data-testid="links-empty">
|
||||
<p>
|
||||
{{ $options.i18n.emptyStateMessage }}
|
||||
</p>
|
||||
<gl-button
|
||||
v-if="!isShownAddForm"
|
||||
category="secondary"
|
||||
variant="confirm"
|
||||
data-testid="toggle-add-form"
|
||||
@click="toggleAddForm"
|
||||
>
|
||||
{{ $options.i18n.addChildButtonLabel }}
|
||||
</gl-button>
|
||||
<work-item-links-form v-else data-testid="add-links-form" @cancel="toggleAddForm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlForm,
|
||||
GlFormInput,
|
||||
GlButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
relatedWorkItem: '',
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form @submit.prevent>
|
||||
<gl-form-input v-model="relatedWorkItem" class="gl-mb-4" />
|
||||
<gl-button type="submit" category="secondary" variant="confirm">
|
||||
{{ s__('WorkItem|Add') }}
|
||||
</gl-button>
|
||||
<gl-button category="tertiary" class="gl-float-right" @click="$emit('cancel')">
|
||||
{{ s__('WorkItem|Cancel') }}
|
||||
</gl-button>
|
||||
</gl-form>
|
||||
</template>
|
|
@ -358,8 +358,8 @@
|
|||
line-height: 1;
|
||||
padding: 0;
|
||||
min-width: 16px;
|
||||
color: $gray-400;
|
||||
fill: $gray-400;
|
||||
color: $gray-500;
|
||||
fill: $gray-500;
|
||||
|
||||
svg {
|
||||
@include btn-svg;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'digest/md5'
|
||||
require 'uri'
|
||||
|
||||
module ApplicationHelper
|
||||
|
|
|
@ -15,5 +15,18 @@ module Emails
|
|||
email = user.notification_email_or_default
|
||||
mail to: email, subject: "Unsubscribed from GitLab administrator notifications"
|
||||
end
|
||||
|
||||
def user_auto_banned_email(admin_id, user_id, max_project_downloads:, within_seconds:)
|
||||
admin = User.find(admin_id)
|
||||
@user = User.find(user_id)
|
||||
@max_project_downloads = max_project_downloads
|
||||
@within_minutes = within_seconds / 60
|
||||
|
||||
Gitlab::I18n.with_locale(admin.preferred_language) do
|
||||
email_with_layout(
|
||||
to: admin.notification_email_or_default,
|
||||
subject: subject(_("We've detected unusual activity")))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,11 +8,9 @@ module Emails
|
|||
|
||||
add_project_headers
|
||||
|
||||
mail(to: recipient,
|
||||
subject: auto_devops_disabled_subject(@project.name)) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: recipient,
|
||||
subject: auto_devops_disabled_subject(@project.name))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -94,10 +94,9 @@ module Emails
|
|||
@project = Project.find(project_id)
|
||||
@results = results
|
||||
|
||||
mail(to: @user.notification_email_for(@project.group), subject: subject('Imported issues')) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: @user.notification_email_for(@project.group),
|
||||
subject: subject('Imported issues'))
|
||||
end
|
||||
|
||||
def issues_csv_email(user, project, csv_data, export_status)
|
||||
|
@ -110,10 +109,9 @@ module Emails
|
|||
|
||||
filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv"
|
||||
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
|
||||
mail(to: user.notification_email_for(@project.group), subject: subject("Exported issues")) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(@project.group),
|
||||
subject: subject("Exported issues"))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -21,7 +21,7 @@ module Emails
|
|||
|
||||
user = User.find(recipient_id)
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(notification_group),
|
||||
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
|
||||
end
|
||||
|
@ -32,7 +32,7 @@ module Emails
|
|||
|
||||
return unless member_exists?
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: member.user.notification_email_for(notification_group),
|
||||
subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
|
||||
end
|
||||
|
@ -47,7 +47,7 @@ module Emails
|
|||
|
||||
human_name = @source_hidden ? 'Hidden' : member_source.human_name
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(notification_group),
|
||||
subject: subject("Access to the #{human_name} #{member_source.model_name.singular} was denied"))
|
||||
end
|
||||
|
@ -83,7 +83,7 @@ module Emails
|
|||
|
||||
subject_line = subjects[reminder_index] % { inviter: member.created_by.name }
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
layout: 'unknown_user_mailer',
|
||||
to: member.invite_email,
|
||||
subject: subject(subject_line)
|
||||
|
@ -97,7 +97,7 @@ module Emails
|
|||
return unless member_exists?
|
||||
return unless member.created_by
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: member.created_by.notification_email_for(notification_group),
|
||||
subject: subject('Invitation accepted'))
|
||||
end
|
||||
|
@ -111,7 +111,7 @@ module Emails
|
|||
|
||||
user = User.find(created_by_id)
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(notification_group),
|
||||
subject: subject('Invitation declined'))
|
||||
end
|
||||
|
@ -128,7 +128,7 @@ module Emails
|
|||
_('Group membership expiration date removed')
|
||||
end
|
||||
|
||||
member_email_with_layout(
|
||||
email_with_layout(
|
||||
to: member.user.notification_email_for(notification_group),
|
||||
subject: subject(subject))
|
||||
end
|
||||
|
@ -176,13 +176,6 @@ module Emails
|
|||
def member_source_class
|
||||
@member_source_type.classify.constantize
|
||||
end
|
||||
|
||||
def member_email_with_layout(to:, subject:, layout: 'mailer')
|
||||
mail(to: to, subject: subject) do |format|
|
||||
format.html { render layout: layout }
|
||||
format.text { render layout: layout }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -149,10 +149,9 @@ module Emails
|
|||
|
||||
filename = "#{project.full_path.parameterize}_merge_requests_#{Date.current.iso8601}.csv"
|
||||
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
|
||||
mail(to: user.notification_email_for(@project.group), subject: subject("Exported merge requests")) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(@project.group),
|
||||
subject: subject("Exported merge requests"))
|
||||
end
|
||||
|
||||
def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil)
|
||||
|
|
|
@ -30,11 +30,9 @@ module Emails
|
|||
|
||||
add_headers
|
||||
|
||||
mail(to: recipient,
|
||||
subject: subject(pipeline_subject(status))) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: recipient,
|
||||
subject: subject(pipeline_subject(status)))
|
||||
end
|
||||
|
||||
def add_headers
|
||||
|
|
|
@ -13,7 +13,7 @@ module Emails
|
|||
@user = user
|
||||
@recipient = recipient
|
||||
|
||||
profile_email_with_layout(
|
||||
email_with_layout(
|
||||
to: recipient.notification_email_or_default,
|
||||
subject: subject(_("GitLab Account Request")))
|
||||
end
|
||||
|
@ -21,7 +21,7 @@ module Emails
|
|||
def user_admin_rejection_email(name, email)
|
||||
@name = name
|
||||
|
||||
profile_email_with_layout(
|
||||
email_with_layout(
|
||||
to: email,
|
||||
subject: subject(_("GitLab account request rejected")))
|
||||
end
|
||||
|
@ -29,7 +29,7 @@ module Emails
|
|||
def user_deactivated_email(name, email)
|
||||
@name = name
|
||||
|
||||
profile_email_with_layout(
|
||||
email_with_layout(
|
||||
to: email,
|
||||
subject: subject(_('Your account has been deactivated')))
|
||||
end
|
||||
|
@ -125,7 +125,7 @@ module Emails
|
|||
@target_url = edit_profile_password_url
|
||||
|
||||
Gitlab::I18n.with_locale(@user.preferred_language) do
|
||||
profile_email_with_layout(
|
||||
email_with_layout(
|
||||
to: @user.notification_email_or_default,
|
||||
subject: subject(_("%{host} sign-in from new location") % { host: Gitlab.config.gitlab.host }))
|
||||
end
|
||||
|
@ -151,15 +151,6 @@ module Emails
|
|||
mail(to: @user.notification_email_or_default, subject: subject(_("New email address added")))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def profile_email_with_layout(to:, subject:, layout: 'mailer')
|
||||
mail(to: to, subject: subject) do |format|
|
||||
format.html { render layout: layout }
|
||||
format.text { render layout: layout }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -75,11 +75,9 @@ module Emails
|
|||
subject_text = "Action required: Project #{project.name} is scheduled to be deleted on " \
|
||||
"#{deletion_date} due to inactivity"
|
||||
|
||||
mail(to: user.notification_email_for(project.group),
|
||||
subject: subject(subject_text)) do |format|
|
||||
format.html { render layout: 'mailer' }
|
||||
format.text { render layout: 'mailer' }
|
||||
end
|
||||
email_with_layout(
|
||||
to: user.notification_email_for(project.group),
|
||||
subject: subject(subject_text))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -222,6 +222,13 @@ class Notify < ApplicationMailer
|
|||
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
|
||||
@unsubscribe_url = unsubscribe_sent_notification_url(@sent_notification)
|
||||
end
|
||||
|
||||
def email_with_layout(to:, subject:, layout: 'mailer')
|
||||
mail(to: to, subject: subject) do |format|
|
||||
format.html { render layout: layout }
|
||||
format.text { render layout: layout }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Notify.prepend_mod_with('Notify')
|
||||
|
|
|
@ -205,6 +205,10 @@ class NotifyPreview < ActionMailer::Preview
|
|||
Notify.inactive_project_deletion_warning_email(project, user, '2022-04-22').message
|
||||
end
|
||||
|
||||
def user_auto_banned_email
|
||||
::Notify.user_auto_banned_email(user.id, user.id, max_project_downloads: 5, within_seconds: 600).message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'digest/md5'
|
||||
|
||||
class Key < ApplicationRecord
|
||||
include AfterCommitQueue
|
||||
include Sortable
|
||||
|
|
|
@ -121,16 +121,16 @@ module MergeRequests
|
|||
override :handle_quick_actions
|
||||
def handle_quick_actions(merge_request)
|
||||
super
|
||||
handle_wip_event(merge_request)
|
||||
handle_draft_event(merge_request)
|
||||
end
|
||||
|
||||
def handle_wip_event(merge_request)
|
||||
if wip_event = params.delete(:wip_event)
|
||||
def handle_draft_event(merge_request)
|
||||
if draft_event = params.delete(:wip_event)
|
||||
# We update the title that is provided in the params or we use the mr title
|
||||
title = params[:title] || merge_request.title
|
||||
params[:title] = case wip_event
|
||||
when 'wip' then MergeRequest.wip_title(title)
|
||||
when 'unwip' then MergeRequest.wipless_title(title)
|
||||
params[:title] = case draft_event
|
||||
when 'wip' then MergeRequest.draft_title(title)
|
||||
when 'unwip' then MergeRequest.draftless_title(title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ module MergeRequests
|
|||
:source_branch_ref,
|
||||
:source_project,
|
||||
:compare_commits,
|
||||
:wip_title,
|
||||
:draft_title,
|
||||
:description,
|
||||
:first_multiline_commit,
|
||||
:errors,
|
||||
|
@ -206,7 +206,7 @@ module MergeRequests
|
|||
def set_draft_title_if_needed
|
||||
return unless compare_commits.empty? || Gitlab::Utils.to_boolean(params[:draft])
|
||||
|
||||
merge_request.title = wip_title
|
||||
merge_request.title = draft_title
|
||||
end
|
||||
|
||||
# When your branch name starts with an iid followed by a dash this pattern will be
|
||||
|
|
|
@ -43,3 +43,5 @@ module MergeRequests
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
MergeRequests::ReloadMergeHeadDiffService.prepend_mod
|
||||
|
|
|
@ -24,12 +24,14 @@ module Packages
|
|||
file.write(content)
|
||||
file.flush
|
||||
|
||||
md5 = Gitlab::FIPS.enabled? ? nil : Digest::MD5.hexdigest(content)
|
||||
|
||||
package.package_files.create!(
|
||||
file: file,
|
||||
size: file.size,
|
||||
file_name: "#{gemspec.name}.gemspec",
|
||||
file_sha1: Digest::SHA1.hexdigest(content),
|
||||
file_md5: Digest::MD5.hexdigest(content),
|
||||
file_md5: md5,
|
||||
file_sha256: Digest::SHA256.hexdigest(content)
|
||||
)
|
||||
ensure
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
%button.gl-button.btn.btn-default.award-control.has-tooltip.js-add-award{ type: 'button',
|
||||
'aria-label': _('Add reaction'),
|
||||
data: { title: _('Add reaction') } }
|
||||
%span{ class: "award-control-icon award-control-icon-neutral" }= sprite_icon('slight-smile')
|
||||
%span{ class: "award-control-icon award-control-icon-positive" }= sprite_icon('smiley')
|
||||
%span{ class: "award-control-icon award-control-icon-super-positive" }= sprite_icon('smile')
|
||||
%span{ class: "award-control-icon award-control-icon-neutral gl-icon" }= sprite_icon('slight-smile')
|
||||
%span{ class: "award-control-icon award-control-icon-positive gl-icon" }= sprite_icon('smiley')
|
||||
%span{ class: "award-control-icon award-control-icon-super-positive gl-icon" }= sprite_icon('smile')
|
||||
= yield
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
|
||||
- link_end = '</a>'.html_safe
|
||||
= email_default_heading(_("We've detected some unusual activity"))
|
||||
%p
|
||||
= _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes }
|
||||
%p
|
||||
= _('If this is a mistake, you can %{link_start}unban them%{link_end}.').html_safe % { link_start: link_start % { url: admin_users_url(filter: 'banned') }, link_end: link_end }
|
||||
%p
|
||||
= _('You can adjust rules on auto-banning %{link_start}here%{link_end}.').html_safe % { link_start: link_start % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') }, link_end: link_end }
|
|
@ -0,0 +1,7 @@
|
|||
<%= _("We've detected some unusual activity") %>
|
||||
|
||||
<%= _('We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes.') % { username: sanitize_name(@user.name), max_project_downloads: @max_project_downloads, within_minutes: @within_minutes } %>
|
||||
|
||||
<%= _('If this is a mistake, you can unban them: %{url}.') % { url: admin_users_url(filter: 'banned') } %>
|
||||
|
||||
<%= _('You can adjust rules on auto-banning here: %{url}.') % { url: network_admin_application_settings_url(anchor: 'js-ip-limits-settings') } %>
|
|
@ -9,15 +9,14 @@
|
|||
- if can?(current_user, :award_emoji, note)
|
||||
- if note.emoji_awardable?
|
||||
.note-actions-item
|
||||
= button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary btn-transparent", data: { position: 'right', container: 'body' } do
|
||||
= sprite_icon('slight-smile', css_class: 'link-highlight award-control-icon-neutral gl-button-icon gl-icon gl-text-gray-400')
|
||||
= sprite_icon('smiley', css_class: 'link-highlight award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
|
||||
= sprite_icon('smile', css_class: 'link-highlight award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
|
||||
= button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary", data: { position: 'right', container: 'body' } do
|
||||
= sprite_icon('slight-smile', css_class: 'award-control-icon-neutral gl-button-icon gl-icon')
|
||||
= sprite_icon('smiley', css_class: 'award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
|
||||
= sprite_icon('smile', css_class: 'award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
|
||||
|
||||
- if note_editable
|
||||
.note-actions-item.gl-ml-0
|
||||
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-px-2!', data: { container: 'body', qa_selector: 'edit_comment_button' } do
|
||||
%span.link-highlight
|
||||
= sprite_icon('pencil', css_class: 'gl-button-icon gl-icon gl-text-gray-400 s16')
|
||||
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn gl-button btn-default-tertiary btn-icon', data: { container: 'body', qa_selector: 'edit_comment_button' } do
|
||||
= sprite_icon('pencil', css_class: 'gl-button-icon gl-icon')
|
||||
|
||||
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
- if note_editable || !is_current_user
|
||||
%div{ class: "dropdown more-actions note-actions-item gl-ml-0!" }
|
||||
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-pl-2! gl-pr-0!', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do
|
||||
= sprite_icon('ellipsis_v', css_class: 'gl-button-icon gl-icon gl-text-gray-400')
|
||||
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn gl-button btn-default-tertiary btn-icon', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do
|
||||
= sprite_icon('ellipsis_v', css_class: 'gl-button-icon gl-icon')
|
||||
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
|
||||
%li
|
||||
= clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
.note-actions-item
|
||||
= link_to '#', title: _('Add reaction'), class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do
|
||||
%span{ class: 'link-highlight award-control-icon-neutral' }= sprite_icon('slight-smile')
|
||||
%span{ class: 'link-highlight award-control-icon-positive' }= sprite_icon('smiley')
|
||||
%span{ class: 'link-highlight award-control-icon-super-positive' }= sprite_icon('smile')
|
||||
= link_to '#', title: _('Add reaction'), class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary", data: { position: 'right' } do
|
||||
= sprite_icon('slight-smile', css_class: 'award-control-icon-neutral gl-button-icon gl-icon')
|
||||
= sprite_icon('smiley', css_class: 'award-control-icon-positive gl-button-icon gl-icon gl-left-3!')
|
||||
= sprite_icon('smile', css_class: 'award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ')
|
||||
|
||||
- if note_editable
|
||||
.note-actions-item
|
||||
= button_tag title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip gl-button btn btn-transparent', data: { container: 'body', qa_selector: 'edit_comment_button' } do
|
||||
%span.link-highlight
|
||||
= custom_icon('icon_pencil')
|
||||
.note-actions-item.gl-ml-0
|
||||
= button_tag title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip gl-button btn btn-default-tertiary btn-icon', data: { container: 'body', qa_selector: 'edit_comment_button' } do
|
||||
= sprite_icon('pencil')
|
||||
|
||||
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'settingslogic'
|
||||
require 'digest/md5'
|
||||
|
||||
class Settings < Settingslogic
|
||||
source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'digest/md5'
|
||||
|
||||
REVIEW_ROULETTE_SECTION = <<MARKDOWN
|
||||
## Reviewer roulette
|
||||
MARKDOWN
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'digest/md5'
|
||||
|
||||
class Gitlab::Seeder::GroupLabels
|
||||
def initialize(group, label_per_group: 10)
|
||||
@group = group
|
||||
|
|
|
@ -4,13 +4,13 @@ group: Pipeline Execution
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Pipeline triggers API **(FREE)**
|
||||
# Pipeline trigger tokens API **(FREE)**
|
||||
|
||||
You can read more about [triggering pipelines through the API](../ci/triggers/index.md).
|
||||
|
||||
## List project triggers
|
||||
## List project trigger tokens
|
||||
|
||||
Get a list of project's build triggers.
|
||||
Get a list of a project's pipeline trigger tokens.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/triggers
|
||||
|
@ -41,9 +41,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
The trigger token is displayed in full if the trigger token was created by the authenticated
|
||||
user. Trigger tokens created by other users are shortened to four characters.
|
||||
|
||||
## Get trigger details
|
||||
## Get trigger token details
|
||||
|
||||
Get details of project's build trigger.
|
||||
Get details of a project's pipeline trigger.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/triggers/:trigger_id
|
||||
|
@ -70,9 +70,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
}
|
||||
```
|
||||
|
||||
## Create a project trigger
|
||||
## Create a trigger token
|
||||
|
||||
Create a trigger for a project.
|
||||
Create a pipeline trigger for a project.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/triggers
|
||||
|
@ -100,9 +100,9 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
}
|
||||
```
|
||||
|
||||
## Update a project trigger
|
||||
## Update a project trigger token
|
||||
|
||||
Update a trigger for a project.
|
||||
Update a pipeline trigger token for a project.
|
||||
|
||||
```plaintext
|
||||
PUT /projects/:id/triggers/:trigger_id
|
||||
|
@ -131,9 +131,9 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
}
|
||||
```
|
||||
|
||||
## Remove a project trigger
|
||||
## Remove a project trigger token
|
||||
|
||||
Remove a project's build trigger.
|
||||
Remove a project's pipeline trigger token.
|
||||
|
||||
```plaintext
|
||||
DELETE /projects/:id/triggers/:trigger_id
|
||||
|
@ -147,3 +147,78 @@ DELETE /projects/:id/triggers/:trigger_id
|
|||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
|
||||
```
|
||||
|
||||
## Trigger a pipeline with a token
|
||||
|
||||
Trigger a pipeline by using a pipeline [trigger token](../ci/triggers/index.md#create-a-trigger-token)
|
||||
or a [CI/CD job token](../ci/jobs/ci_job_token.md) for authentication.
|
||||
|
||||
With a CI/CD job token, the [triggered pipeline is a multi-project pipeline](../ci/jobs/ci_job_token.md#trigger-a-multi-project-pipeline-by-using-a-cicd-job-token).
|
||||
The job that authenticates the request becomes associated with the upstream pipeline,
|
||||
which is visible on the [pipeline graph](../ci/pipelines/multi_project_pipelines.md#multi-project-pipeline-visualization).
|
||||
|
||||
If you use a trigger token in a job, the job is not associated with the upstream pipeline.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/trigger/pipeline
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:------------|:---------------|:-----------------------|:---------------------|
|
||||
| `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `ref` | string | **{check-circle}** Yes | The branch or tag to run the pipeline on. |
|
||||
| `token` | string | **{check-circle}** Yes | The trigger token or CI/CD job token. |
|
||||
| `variables` | array | **{dotted-circle}** No | An array containing the variables available in the pipeline, matching the structure `[{ 'key': 'UPLOAD_TO_S3', 'variable_type': 'file', 'value': 'true' }, {'key': 'TEST', 'value': 'test variable'}]`. If `variable_type` is excluded, it defaults to `env_var`. |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST "https://gitlab.example.com/api/v4/projects/123/trigger/pipeline?token=2cb1840fb9dfc9fb0b7b1609cd29cb&ref=main"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 257,
|
||||
"iid": 118,
|
||||
"project_id": 21,
|
||||
"sha": "91e2711a93e5d9e8dddfeb6d003b636b25bf6fc9",
|
||||
"ref": "main",
|
||||
"status": "created",
|
||||
"source": "trigger",
|
||||
"created_at": "2022-03-31T01:12:49.068Z",
|
||||
"updated_at": "2022-03-31T01:12:49.068Z",
|
||||
"web_url": "http://127.0.0.1:3000/test-group/test-project/-/pipelines/257",
|
||||
"before_sha": "0000000000000000000000000000000000000000",
|
||||
"tag": false,
|
||||
"yaml_errors": null,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "root",
|
||||
"name": "Administrator",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||
"web_url": "http://127.0.0.1:3000/root"
|
||||
},
|
||||
"started_at": null,
|
||||
"finished_at": null,
|
||||
"committed_at": null,
|
||||
"duration": null,
|
||||
"queued_duration": null,
|
||||
"coverage": null,
|
||||
"detailed_status": {
|
||||
"icon": "status_created",
|
||||
"text": "created",
|
||||
"label": "created",
|
||||
"group": "created",
|
||||
"tooltip": "created",
|
||||
"has_details": true,
|
||||
"details_path": "/test-group/test-project/-/pipelines/257",
|
||||
"illustration": null,
|
||||
"favicon": "/assets/ci_favicons/favicon_status_created-4b975aa976d24e5a3ea7cd9a5713e6ce2cd9afd08b910415e96675de35f64955.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -104,13 +104,12 @@ The job token scope is only for controlling access to private projects.
|
|||
There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to improve
|
||||
the feature with more strategic control of the access permissions.
|
||||
|
||||
## Trigger a multi-project pipeline by using a CI job token
|
||||
## Trigger a multi-project pipeline by using a CI/CD job token
|
||||
|
||||
> `CI_JOB_TOKEN` for multi-project pipelines was [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) from GitLab Premium to GitLab Free in 12.4.
|
||||
|
||||
You can use the `CI_JOB_TOKEN` to trigger [multi-project pipelines](../pipelines/multi_project_pipelines.md)
|
||||
from a CI/CD job. A pipeline triggered this way creates a dependent pipeline relation
|
||||
that is visible on the [pipeline graph](../pipelines/multi_project_pipelines.md#multi-project-pipeline-visualization).
|
||||
You can use the `CI_JOB_TOKEN` to [trigger multi-project pipelines](../../api/pipeline_triggers.md#trigger-a-pipeline-with-a-token)
|
||||
from a CI/CD job.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
|
@ -72,11 +72,16 @@ For configuration information, see
|
|||
|
||||
### Git operations using SSH
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78373) in GitLab 14.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78373) in GitLab 14.7 [with a flag](../administration/feature_flags.md) named `rate_limit_gitlab_shell`. Disabled by default.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79419) in GitLab 14.8.
|
||||
|
||||
GitLab rate limits Git operations using SSH by user account and project. If a request from a user for a Git operation
|
||||
on a project exceeds the rate limit, GitLab drops further connection requests from that user for the project.
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To disable the feature, ask an administrator to
|
||||
[disable the feature flag](../administration/feature_flags.md) named `rate_limit_gitlab_shell`. On GitLab.com, this feature
|
||||
is available.
|
||||
|
||||
GitLab applies rate limits to Git operations that use SSH by user account and project. When the rate limit is exceeded, GitLab rejects
|
||||
further connection requests from that user for the project.
|
||||
|
||||
The rate limit applies at the Git command ([plumbing](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain)) level.
|
||||
Each command has a rate limit of 600 per minute. For example:
|
||||
|
@ -86,9 +91,8 @@ Each command has a rate limit of 600 per minute. For example:
|
|||
|
||||
Because the same commands are shared by `git-upload-pack`, `git pull`, and `git clone`, they share a rate limit.
|
||||
|
||||
The requests/minute threshold for this rate limit is not configurable. Self-managed customers can disable this
|
||||
rate limit by [disabling the feature flag](../administration/feature_flags.md#enable-or-disable-the-feature)
|
||||
with `Feature.disable(:rate_limit_gitlab_shell)`.
|
||||
The requests per minute threshold for this rate limit is not configurable. Self-managed customers can disable this
|
||||
rate limit.
|
||||
|
||||
### Repository archives
|
||||
|
||||
|
|
|
@ -10,11 +10,6 @@ The [certificate-based Kubernetes integration with GitLab](../index.md)
|
|||
was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8)
|
||||
in GitLab 14.5. To connect your clusters, use the [GitLab agent](../../../clusters/agent/index.md).
|
||||
|
||||
<!-- TBA: (We need to resolve https://gitlab.com/gitlab-org/gitlab/-/issues/343660 before adding this line)
|
||||
If you don't have a cluster yet, create one and connect it to GitLab through the agent.
|
||||
You can also create a new cluster from GitLab using [Infrastructure as Code](../../iac/index.md#create-a-new-cluster-through-iac).
|
||||
-->
|
||||
|
||||
## Cluster levels (DEPRECATED)
|
||||
|
||||
> [Deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5.
|
||||
|
|
|
@ -32,8 +32,7 @@ To get started, choose the template that best suits your needs:
|
|||
|
||||
All templates:
|
||||
|
||||
- Use the [GitLab-managed Terraform state](#gitlab-managed-terraform-state) as
|
||||
the Terraform state storage backend.
|
||||
- Use the [GitLab-managed Terraform state](terraform_state.md) as the Terraform state storage backend.
|
||||
- Trigger four pipeline stages: `test`, `validate`, `build`, and `deploy`.
|
||||
- Run Terraform commands: `test`, `validate`, `plan`, and `plan-json`. It also runs the `apply` only on the default branch.
|
||||
- Run the [Terraform SAST scanner](../../application_security/iac_scanning/index.md#configure-iac-scanning-manually).
|
||||
|
@ -89,37 +88,19 @@ To use a Terraform template:
|
|||
# TF_ROOT: terraform/production
|
||||
```
|
||||
|
||||
1. (Optional) Override in your `.gitlab-ci.yml` file the attributes present
|
||||
1. Optional. Override in your `.gitlab-ci.yml` file the attributes present
|
||||
in the template you fetched to customize your configuration.
|
||||
|
||||
## GitLab-managed Terraform state
|
||||
|
||||
Use the [GitLab-managed Terraform state](terraform_state.md) to store state
|
||||
files in local storage or in a remote store of your choice.
|
||||
|
||||
## Terraform module registry
|
||||
|
||||
Use GitLab as a [Terraform module registry](../../packages/terraform_module_registry/index.md)
|
||||
to create and publish Terraform modules to a private registry.
|
||||
|
||||
## Terraform integration in merge requests
|
||||
|
||||
Use the [Terraform integration in merge requests](mr_integration.md)
|
||||
to collaborate on Terraform code changes and Infrastructure-as-Code
|
||||
workflows.
|
||||
|
||||
## The GitLab Terraform provider
|
||||
|
||||
The [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) is a Terraform plugin to facilitate
|
||||
managing of GitLab resources such as users, groups, and projects. It is released separately from GitLab
|
||||
and its documentation is available on [Terraform](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs).
|
||||
|
||||
## Create a new cluster through IaC
|
||||
|
||||
- Learn how to [create a new cluster on Amazon Elastic Kubernetes Service (EKS)](../clusters/connect/new_eks_cluster.md).
|
||||
- Learn how to [create a new cluster on Google Kubernetes Engine (GKE)](../clusters/connect/new_gke_cluster.md).
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Terraform images](https://gitlab.com/gitlab-org/terraform-images).
|
||||
- [Troubleshooting](troubleshooting.md) issues with GitLab and Terraform.
|
||||
- View [the images that contain the `gitlab-terraform` shell script](https://gitlab.com/gitlab-org/terraform-images).
|
||||
- Use GitLab as a [Terraform module registry](../../packages/terraform_module_registry/index.md).
|
||||
- To store state files in local storage or in a remote store, use the [GitLab-managed Terraform state](terraform_state.md).
|
||||
- To collaborate on Terraform code changes and Infrastructure-as-Code workflows, use the
|
||||
[Terraform integration in merge requests](mr_integration.md).
|
||||
- To manage GitLab resources like users, groups, and projects, use the
|
||||
[GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab). It is released separately from GitLab
|
||||
and its documentation is available on [the Terraform docs site](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs).
|
||||
- [Create a new cluster on Amazon Elastic Kubernetes Service (EKS)](../clusters/connect/new_eks_cluster.md).
|
||||
- [Create a new cluster on Google Kubernetes Engine (GKE)](../clusters/connect/new_gke_cluster.md).
|
||||
- [Troubleshoot](troubleshooting.md) issues with GitLab and Terraform.
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - [Deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5.
|
||||
|
||||
WARNING:
|
||||
This feature was deprecated in GitLab 14.5. Use [Infrastructure as Code](../../infrastructure/iac/index.md#create-a-new-cluster-through-iac)
|
||||
This feature was deprecated in GitLab 14.5. Use [Infrastructure as Code](../../infrastructure/iac/index.md)
|
||||
to create new clusters.
|
||||
|
||||
Through GitLab, you can create new clusters and add existing clusters hosted on Amazon Elastic
|
||||
|
@ -23,7 +23,7 @@ use the [GitLab agent](../../clusters/agent/index.md).
|
|||
|
||||
## Create a new EKS cluster
|
||||
|
||||
To create a new cluster from GitLab, use [Infrastructure as Code](../../infrastructure/iac/index.md#create-a-new-cluster-through-iac).
|
||||
To create a new cluster from GitLab, use [Infrastructure as Code](../../infrastructure/iac/index.md).
|
||||
|
||||
### How to create a new cluster on EKS through cluster certificates (DEPRECATED)
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/327908) in GitLab 14.0.
|
||||
To create and manage a new cluster use [Infrastructure as Code](../../infrastructure/iac/index.md#create-a-new-cluster-through-iac).
|
||||
To create and manage a new cluster use [Infrastructure as Code](../../infrastructure/iac/index.md).
|
||||
|
||||
## Disable a cluster
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
module Gitlab
|
||||
module Database
|
||||
class ConsistencyChecker
|
||||
BATCH_SIZE = 1000
|
||||
MAX_BATCHES = 25
|
||||
MAX_RUNTIME = 30.seconds # must be less than the scheduling frequency of the ConsistencyCheck jobs
|
||||
BATCH_SIZE = 500
|
||||
MAX_BATCHES = 20
|
||||
MAX_RUNTIME = 5.seconds # must be less than the scheduling frequency of the ConsistencyCheck jobs
|
||||
|
||||
delegate :monotonic_time, to: :'Gitlab::Metrics::System'
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ module Gitlab
|
|||
else
|
||||
# Only show what is new in the source branch
|
||||
# compared to the target branch, not the other way
|
||||
# around. The linex below with merge_base is
|
||||
# around. The line below with merge_base is
|
||||
# equivalent to diff with three dots (git diff
|
||||
# branch1...branch2) From the git documentation:
|
||||
# "git diff A...B" is equivalent to "git diff
|
||||
|
|
|
@ -4,60 +4,6 @@ require 'logger'
|
|||
|
||||
namespace :gitlab do
|
||||
namespace :pages do
|
||||
desc "GitLab | Pages | Migrate legacy storage to zip format"
|
||||
task migrate_legacy_storage: :gitlab_environment do
|
||||
logger.info('Starting to migrate legacy pages storage to zip deployments')
|
||||
|
||||
result = ::Pages::MigrateFromLegacyStorageService.new(logger,
|
||||
ignore_invalid_entries: ignore_invalid_entries,
|
||||
mark_projects_as_not_deployed: mark_projects_as_not_deployed)
|
||||
.execute_with_threads(threads: migration_threads, batch_size: batch_size)
|
||||
|
||||
logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.")
|
||||
logger.info("- The #{result[:migrated]} projects migrated successfully")
|
||||
logger.info("- The #{result[:errored]} projects failed to be migrated")
|
||||
end
|
||||
|
||||
desc "GitLab | Pages | DANGER: Removes data which was migrated from legacy storage on zip storage. Can be used if some bugs in migration are discovered and migration needs to be restarted from scratch."
|
||||
task clean_migrated_zip_storage: :gitlab_environment do
|
||||
destroyed_deployments = 0
|
||||
|
||||
logger.info("Starting to delete migrated pages deployments")
|
||||
|
||||
::PagesDeployment.migrated_from_legacy_storage.each_batch(of: batch_size) do |batch|
|
||||
destroyed_deployments += batch.count
|
||||
|
||||
# we need to destroy associated files, so can't use delete_all
|
||||
batch.destroy_all # rubocop: disable Cop/DestroyAll
|
||||
|
||||
logger.info("#{destroyed_deployments} deployments were deleted")
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Logger.new($stdout)
|
||||
end
|
||||
|
||||
def migration_threads
|
||||
ENV.fetch('PAGES_MIGRATION_THREADS', '3').to_i
|
||||
end
|
||||
|
||||
def batch_size
|
||||
ENV.fetch('PAGES_MIGRATION_BATCH_SIZE', '10').to_i
|
||||
end
|
||||
|
||||
def ignore_invalid_entries
|
||||
Gitlab::Utils.to_boolean(
|
||||
ENV.fetch('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'false')
|
||||
)
|
||||
end
|
||||
|
||||
def mark_projects_as_not_deployed
|
||||
Gitlab::Utils.to_boolean(
|
||||
ENV.fetch('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'false')
|
||||
)
|
||||
end
|
||||
|
||||
namespace :deployments do
|
||||
task migrate_to_object_storage: :gitlab_environment do
|
||||
logger = Logger.new($stdout)
|
||||
|
|
|
@ -19162,6 +19162,12 @@ msgstr ""
|
|||
msgid "If this email was added in error, you can remove it here: %{profile_emails_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If this is a mistake, you can %{link_start}unban them%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "If this is a mistake, you can unban them: %{url}."
|
||||
msgstr ""
|
||||
|
||||
msgid "If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -42469,6 +42475,9 @@ msgstr ""
|
|||
msgid "We want to be sure it is you, please confirm you are not a robot."
|
||||
msgstr ""
|
||||
|
||||
msgid "We want to let you know %{username} has been banned from your GitLab instance due to them downloading more than %{max_project_downloads} project repositories within %{within_minutes} minutes."
|
||||
msgstr ""
|
||||
|
||||
msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders."
|
||||
msgstr ""
|
||||
|
||||
|
@ -42484,6 +42493,12 @@ msgstr ""
|
|||
msgid "We're experiencing difficulties and this tab content is currently unavailable."
|
||||
msgstr ""
|
||||
|
||||
msgid "We've detected some unusual activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "We've detected unusual activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "We've found no vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43077,9 +43092,21 @@ msgstr ""
|
|||
msgid "Work in progress Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add a child"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to delete the work item? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Child items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Convert to work item"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43092,6 +43119,9 @@ msgstr ""
|
|||
msgid "WorkItem|New Task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43304,6 +43334,12 @@ msgstr ""
|
|||
msgid "You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You can adjust rules on auto-banning %{link_start}here%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You can adjust rules on auto-banning here: %{url}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You can also create a project from the command line."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Verify', :runner, :reliable, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
RSpec.describe 'Verify', :runner, :reliable do
|
||||
describe 'Parent-child pipelines dependent relationship' do
|
||||
let!(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Verify', :runner, :reliable, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
RSpec.describe 'Verify', :runner, :reliable do
|
||||
describe 'Parent-child pipelines independent relationship' do
|
||||
let!(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
|
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Package' do
|
||||
describe 'Container Registry', :reliable, only: { subdomain: %i[staging pre] }, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
describe 'Container Registry', :reliable, only: { subdomain: %i[staging pre] } do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'project-with-registry'
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable do
|
||||
describe 'Maven group level endpoint' do
|
||||
include Runtime::Fixtures
|
||||
include_context 'packages registry qa scenario'
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable do
|
||||
describe 'Maven project level endpoint' do
|
||||
let(:group_id) { 'com.gitlab.qa' }
|
||||
let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable, quarantine: {
|
||||
only: { subdomain: %i[staging staging-canary] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/363188",
|
||||
type: :investigating
|
||||
} do
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable do
|
||||
describe 'NuGet group level endpoint' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include Runtime::Fixtures
|
||||
|
|
|
@ -1730,7 +1730,7 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
|
||||
describe 'POST remove_wip' do
|
||||
before do
|
||||
merge_request.title = merge_request.wip_title
|
||||
merge_request.title = merge_request.draft_title
|
||||
merge_request.save!
|
||||
|
||||
post :remove_wip,
|
||||
|
@ -1743,8 +1743,8 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
xhr: true
|
||||
end
|
||||
|
||||
it 'removes the wip status' do
|
||||
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
|
||||
it 'removes the draft status' do
|
||||
expect(merge_request.reload.title).to eq(merge_request.draftless_title)
|
||||
end
|
||||
|
||||
it 'renders MergeRequest as JSON' do
|
||||
|
|
|
@ -56,7 +56,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
|
|||
buttonclass=""
|
||||
buttonicon="archive"
|
||||
buttonsize="medium"
|
||||
buttonvariant="warning"
|
||||
buttonvariant="default"
|
||||
class="gl-ml-3"
|
||||
hasselecteddesigns="true"
|
||||
title="Archive design"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import * as createDefaultClient from '~/lib/graphql';
|
||||
import initMRPopovers from '~/mr_popover/index';
|
||||
import initIssuablePopovers from '~/issuable/popover/index';
|
||||
|
||||
createDefaultClient.default = jest.fn();
|
||||
|
||||
describe('initMRPopovers', () => {
|
||||
describe('initIssuablePopovers', () => {
|
||||
let mr1;
|
||||
let mr2;
|
||||
let mr3;
|
||||
|
@ -14,7 +14,7 @@ describe('initMRPopovers', () => {
|
|||
<div id="one" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
|
||||
MR1
|
||||
</div>
|
||||
<div id="two" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
|
||||
<div id="two" class="gfm-merge_request" title="title" data-iid="1" data-project-path="group/project">
|
||||
MR2
|
||||
</div>
|
||||
<div id="three" class="gfm-merge_request">
|
||||
|
@ -32,14 +32,14 @@ describe('initMRPopovers', () => {
|
|||
});
|
||||
|
||||
it('does not add the same event listener twice', () => {
|
||||
initMRPopovers([mr1, mr1, mr2]);
|
||||
initIssuablePopovers([mr1, mr1, mr2]);
|
||||
|
||||
expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
|
||||
expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not add listener if it does not have the necessary data attributes', () => {
|
||||
initMRPopovers([mr1, mr2, mr3]);
|
||||
initIssuablePopovers([mr1, mr2, mr3]);
|
||||
|
||||
expect(mr3.addEventListener).not.toHaveBeenCalled();
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import MRPopover from '~/mr_popover/components/mr_popover.vue';
|
||||
import MRPopover from '~/issuable/popover/components/mr_popover.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
|
||||
describe('MR Popover', () => {
|
|
@ -1,13 +1,11 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import $ from 'jquery';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import initMRPopovers from '~/mr_popover/index';
|
||||
import createStore from '~/notes/stores';
|
||||
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
jest.mock('~/mr_popover/index', () => jest.fn());
|
||||
|
||||
describe('system note component', () => {
|
||||
let vm;
|
||||
let props;
|
||||
|
@ -76,10 +74,12 @@ describe('system note component', () => {
|
|||
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
|
||||
});
|
||||
|
||||
it('should initMRPopovers onMount', () => {
|
||||
it('should renderGFM onMount', () => {
|
||||
const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
|
||||
|
||||
createComponent(props);
|
||||
|
||||
expect(initMRPopovers).toHaveBeenCalled();
|
||||
expect(renderGFMSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders outdated code lines', async () => {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
|
||||
|
||||
describe('WorkItemLinks', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(WorkItemLinks, { propsData: { workItemId: '123' } });
|
||||
};
|
||||
|
||||
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
||||
const findLinksBody = () => wrapper.findByTestId('links-body');
|
||||
const findEmptyState = () => wrapper.findByTestId('links-empty');
|
||||
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
|
||||
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('is collapsed by default', () => {
|
||||
expect(findToggleButton().props('icon')).toBe('angle-down');
|
||||
expect(findLinksBody().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('expands on click toggle button', async () => {
|
||||
findToggleButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findToggleButton().props('icon')).toBe('angle-up');
|
||||
expect(findLinksBody().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays empty state if there are no links', async () => {
|
||||
findToggleButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
expect(findToggleAddFormButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('add link form', () => {
|
||||
it('displays form on click add button and hides form on cancel', async () => {
|
||||
findToggleButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
|
||||
findToggleAddFormButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findAddLinksForm().exists()).toBe(true);
|
||||
|
||||
findAddLinksForm().vm.$emit('cancel');
|
||||
await nextTick();
|
||||
|
||||
expect(findAddLinksForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -174,7 +174,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
|||
|
||||
context 'with draft argument' do
|
||||
before do
|
||||
merge_request_4.update!(title: MergeRequest.wip_title(merge_request_4.title))
|
||||
merge_request_4.update!(title: MergeRequest.draft_title(merge_request_4.title))
|
||||
end
|
||||
|
||||
context 'with draft: true argument' do
|
||||
|
|
|
@ -3,9 +3,62 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Emails::AdminNotification do
|
||||
include EmailSpec::Matchers
|
||||
include_context 'gitlab email notification'
|
||||
|
||||
it 'adds email methods to Notify' do
|
||||
subject.instance_methods.each do |email_method|
|
||||
expect(Notify).to be_respond_to(email_method)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'user_auto_banned_email' do
|
||||
let_it_be(:admin) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:max_project_downloads) { 5 }
|
||||
let(:time_period) { 600 }
|
||||
|
||||
subject do
|
||||
Notify.user_auto_banned_email(
|
||||
admin.id, user.id,
|
||||
max_project_downloads: max_project_downloads,
|
||||
within_seconds: time_period
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'an email sent from GitLab'
|
||||
it_behaves_like 'it should not have Gmail Actions links'
|
||||
it_behaves_like 'a user cannot unsubscribe through footer link'
|
||||
it_behaves_like 'appearance header and footer enabled'
|
||||
it_behaves_like 'appearance header and footer not enabled'
|
||||
|
||||
it 'is sent to the administrator' do
|
||||
is_expected.to deliver_to admin.email
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject "We've detected unusual activity"
|
||||
end
|
||||
|
||||
it 'includes the name of the user' do
|
||||
is_expected.to have_body_text user.name
|
||||
end
|
||||
|
||||
it 'includes the reason' do
|
||||
is_expected.to have_body_text "due to them downloading more than 5 project repositories within 10 minutes"
|
||||
end
|
||||
|
||||
it 'includes a link to unban the user' do
|
||||
is_expected.to have_body_text admin_users_url(filter: 'banned')
|
||||
end
|
||||
|
||||
it 'includes a link to change the settings' do
|
||||
is_expected.to have_body_text network_admin_application_settings_url(anchor: 'js-ip-limits-settings')
|
||||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text "You're receiving this email because of your account on localhost"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1472,20 +1472,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#wipless_title" do
|
||||
describe "#draftless_title" do
|
||||
subject { build_stubbed(:merge_request) }
|
||||
|
||||
['draft:', 'Draft: ', '[Draft]', '[DRAFT] '].each do |draft_prefix|
|
||||
it "removes a '#{draft_prefix}' prefix" do
|
||||
wipless_title = subject.title
|
||||
draftless_title = subject.title
|
||||
subject.title = "#{draft_prefix}#{subject.title}"
|
||||
|
||||
expect(subject.wipless_title).to eq wipless_title
|
||||
expect(subject.draftless_title).to eq draftless_title
|
||||
end
|
||||
|
||||
it "is satisfies the #work_in_progress? method" do
|
||||
subject.title = "#{draft_prefix}#{subject.title}"
|
||||
subject.title = subject.wipless_title
|
||||
subject.title = subject.draftless_title
|
||||
|
||||
expect(subject.work_in_progress?).to eq false
|
||||
end
|
||||
|
@ -1497,58 +1497,58 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
it "doesn't remove a '#{wip_prefix}' prefix" do
|
||||
subject.title = "#{wip_prefix}#{subject.title}"
|
||||
|
||||
expect(subject.wipless_title).to eq subject.title
|
||||
expect(subject.draftless_title).to eq subject.title
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes only draft prefix from the MR title' do
|
||||
subject.title = 'Draft: Implement feature called draft'
|
||||
|
||||
expect(subject.wipless_title).to eq 'Implement feature called draft'
|
||||
expect(subject.draftless_title).to eq 'Implement feature called draft'
|
||||
end
|
||||
|
||||
it 'does not remove WIP in the middle of the title' do
|
||||
subject.title = 'Something with WIP in the middle'
|
||||
|
||||
expect(subject.wipless_title).to eq subject.title
|
||||
expect(subject.draftless_title).to eq subject.title
|
||||
end
|
||||
|
||||
it 'does not remove Draft in the middle of the title' do
|
||||
subject.title = 'Something with Draft in the middle'
|
||||
|
||||
expect(subject.wipless_title).to eq subject.title
|
||||
expect(subject.draftless_title).to eq subject.title
|
||||
end
|
||||
|
||||
it 'does not remove WIP at the end of the title' do
|
||||
subject.title = 'Something ends with WIP'
|
||||
|
||||
expect(subject.wipless_title).to eq subject.title
|
||||
expect(subject.draftless_title).to eq subject.title
|
||||
end
|
||||
|
||||
it 'does not remove Draft at the end of the title' do
|
||||
subject.title = 'Something ends with Draft'
|
||||
|
||||
expect(subject.wipless_title).to eq subject.title
|
||||
expect(subject.draftless_title).to eq subject.title
|
||||
end
|
||||
end
|
||||
|
||||
describe "#wip_title" do
|
||||
describe "#draft_title" do
|
||||
it "adds the Draft: prefix to the title" do
|
||||
wip_title = "Draft: #{subject.title}"
|
||||
draft_title = "Draft: #{subject.title}"
|
||||
|
||||
expect(subject.wip_title).to eq wip_title
|
||||
expect(subject.draft_title).to eq draft_title
|
||||
end
|
||||
|
||||
it "does not add the Draft: prefix multiple times" do
|
||||
wip_title = "Draft: #{subject.title}"
|
||||
subject.title = subject.wip_title
|
||||
subject.title = subject.wip_title
|
||||
draft_title = "Draft: #{subject.title}"
|
||||
subject.title = subject.draft_title
|
||||
subject.title = subject.draft_title
|
||||
|
||||
expect(subject.wip_title).to eq wip_title
|
||||
expect(subject.draft_title).to eq draft_title
|
||||
end
|
||||
|
||||
it "is satisfies the #work_in_progress? method" do
|
||||
subject.title = subject.wip_title
|
||||
subject.title = subject.draft_title
|
||||
|
||||
expect(subject.work_in_progress?).to eq true
|
||||
end
|
||||
|
|
|
@ -24,5 +24,18 @@ RSpec.describe Packages::Rubygems::CreateGemspecService do
|
|||
expect(gemspec_file.file_sha1).not_to be_nil
|
||||
expect(gemspec_file.file_sha256).not_to be_nil
|
||||
end
|
||||
|
||||
context 'with FIPS mode', :fips_mode do
|
||||
it 'does not generate file_md5' do
|
||||
expect { subject }.to change { package.package_files.count }.by(1)
|
||||
|
||||
gemspec_file = package.package_files.find_by(file_name: "#{gemspec.name}.gemspec")
|
||||
expect(gemspec_file.file).not_to be_nil
|
||||
expect(gemspec_file.size).not_to be_nil
|
||||
expect(gemspec_file.file_md5).to be_nil
|
||||
expect(gemspec_file.file_sha1).not_to be_nil
|
||||
expect(gemspec_file.file_sha256).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -333,14 +333,14 @@ RSpec.describe QuickActions::InterpretService do
|
|||
|
||||
shared_examples 'undraft command' do
|
||||
it 'returns wip_event: "unwip" if content contains /draft' do
|
||||
issuable.update!(title: issuable.wip_title)
|
||||
issuable.update!(title: issuable.draft_title)
|
||||
_, updates, _ = service.execute(content, issuable)
|
||||
|
||||
expect(updates).to eq(wip_event: 'unwip')
|
||||
end
|
||||
|
||||
it 'returns the unwip message' do
|
||||
issuable.update!(title: issuable.wip_title)
|
||||
issuable.update!(title: issuable.draft_title)
|
||||
_, _, message = service.execute(content, issuable)
|
||||
|
||||
expect(message).to eq("Unmarked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.")
|
||||
|
|
|
@ -7,86 +7,6 @@ RSpec.describe 'gitlab:pages', :silence_stdout do
|
|||
Rake.application.rake_require 'tasks/gitlab/pages'
|
||||
end
|
||||
|
||||
describe 'migrate_legacy_storage task' do
|
||||
subject { run_rake_task('gitlab:pages:migrate_legacy_storage') }
|
||||
|
||||
it 'calls migration service' do
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
|
||||
ignore_invalid_entries: false,
|
||||
mark_projects_as_not_deployed: false) do |service|
|
||||
expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'uses PAGES_MIGRATION_THREADS environment variable' do
|
||||
stub_env('PAGES_MIGRATION_THREADS', '5')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
|
||||
ignore_invalid_entries: false,
|
||||
mark_projects_as_not_deployed: false) do |service|
|
||||
expect(service).to receive(:execute_with_threads).with(threads: 5, batch_size: 10).and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'uses PAGES_MIGRATION_BATCH_SIZE environment variable' do
|
||||
stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
|
||||
ignore_invalid_entries: false,
|
||||
mark_projects_as_not_deployed: false) do |service|
|
||||
expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 100).and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'uses PAGES_MIGRATION_IGNORE_INVALID_ENTRIES environment variable' do
|
||||
stub_env('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'true')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
|
||||
ignore_invalid_entries: true,
|
||||
mark_projects_as_not_deployed: false) do |service|
|
||||
expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'uses PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED environment variable' do
|
||||
stub_env('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'true')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
|
||||
ignore_invalid_entries: false,
|
||||
mark_projects_as_not_deployed: true) do |service|
|
||||
expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
describe 'clean_migrated_zip_storage task' do
|
||||
it 'removes only migrated deployments' do
|
||||
regular_deployment = create(:pages_deployment)
|
||||
migrated_deployment = create(:pages_deployment, :migrated)
|
||||
|
||||
regular_deployment.project.update_pages_deployment!(regular_deployment)
|
||||
migrated_deployment.project.update_pages_deployment!(migrated_deployment)
|
||||
|
||||
expect(PagesDeployment.all).to contain_exactly(regular_deployment, migrated_deployment)
|
||||
|
||||
run_rake_task('gitlab:pages:clean_migrated_zip_storage')
|
||||
|
||||
expect(PagesDeployment.all).to contain_exactly(regular_deployment)
|
||||
expect(PagesDeployment.find_by_id(regular_deployment.id)).not_to be_nil
|
||||
expect(PagesDeployment.find_by_id(migrated_deployment.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'gitlab:pages:deployments:migrate_to_object_storage' do
|
||||
subject { run_rake_task('gitlab:pages:deployments:migrate_to_object_storage') }
|
||||
|
||||
|
|
|
@ -43,16 +43,12 @@ type artifactsUploadProcessor struct {
|
|||
// Artifacts is like a Multipart but specific for artifacts upload.
|
||||
func Artifacts(myAPI *api.API, h http.Handler, p Preparer) http.Handler {
|
||||
return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
|
||||
opts, err := p.Prepare(a)
|
||||
if err != nil {
|
||||
helper.Fail500(w, r, fmt.Errorf("UploadArtifacts: error preparing file storage options"))
|
||||
return
|
||||
}
|
||||
|
||||
format := r.URL.Query().Get(ArtifactFormatKey)
|
||||
|
||||
mg := &artifactsUploadProcessor{format: format, SavedFileTracker: SavedFileTracker{Request: r}}
|
||||
interceptMultipartFiles(w, r, h, a, mg, opts)
|
||||
mg := &artifactsUploadProcessor{
|
||||
format: format,
|
||||
SavedFileTracker: SavedFileTracker{Request: r},
|
||||
}
|
||||
interceptMultipartFiles(w, r, h, mg, &eagerAuthorizer{a}, p)
|
||||
}, "/authorize")
|
||||
}
|
||||
|
||||
|
@ -61,8 +57,7 @@ func (a *artifactsUploadProcessor) generateMetadataFromZip(ctx context.Context,
|
|||
defer metaWriter.Close()
|
||||
|
||||
metaOpts := &destination.UploadOpts{
|
||||
LocalTempPath: os.TempDir(),
|
||||
TempFilePrefix: "metadata.gz",
|
||||
LocalTempPath: os.TempDir(),
|
||||
}
|
||||
|
||||
fileName := file.LocalPath
|
||||
|
@ -87,7 +82,7 @@ func (a *artifactsUploadProcessor) generateMetadataFromZip(ctx context.Context,
|
|||
done := make(chan saveResult)
|
||||
go func() {
|
||||
var result saveResult
|
||||
result.FileHandler, result.error = destination.Upload(ctx, metaReader, -1, metaOpts)
|
||||
result.FileHandler, result.error = destination.Upload(ctx, metaReader, -1, "metadata.gz", metaOpts)
|
||||
|
||||
done <- result
|
||||
}()
|
||||
|
|
|
@ -23,7 +23,7 @@ func RequestBody(rails PreAuthorizer, h http.Handler, p Preparer) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
fh, err := destination.Upload(r.Context(), r.Body, r.ContentLength, opts)
|
||||
fh, err := destination.Upload(r.Context(), r.Body, r.ContentLength, "upload", opts)
|
||||
if err != nil {
|
||||
helper.Fail500(w, r, fmt.Errorf("RequestBody: upload failed: %v", err))
|
||||
return
|
||||
|
|
|
@ -113,9 +113,9 @@ type consumer interface {
|
|||
|
||||
// Upload persists the provided reader content to all the location specified in opts. A cleanup will be performed once ctx is Done
|
||||
// Make sure the provided context will not expire before finalizing upload with GitLab Rails.
|
||||
func Upload(ctx context.Context, reader io.Reader, size int64, opts *UploadOpts) (*FileHandler, error) {
|
||||
func Upload(ctx context.Context, reader io.Reader, size int64, name string, opts *UploadOpts) (*FileHandler, error) {
|
||||
fh := &FileHandler{
|
||||
Name: opts.TempFilePrefix,
|
||||
Name: name,
|
||||
RemoteID: opts.RemoteID,
|
||||
RemoteURL: opts.RemoteURL,
|
||||
}
|
||||
|
@ -199,13 +199,13 @@ func Upload(ctx context.Context, reader io.Reader, size int64, opts *UploadOpts)
|
|||
}
|
||||
|
||||
logger := log.WithContextFields(ctx, log.Fields{
|
||||
"copied_bytes": fh.Size,
|
||||
"is_local": opts.IsLocalTempFile(),
|
||||
"is_multipart": opts.IsMultipart(),
|
||||
"is_remote": !opts.IsLocalTempFile(),
|
||||
"remote_id": opts.RemoteID,
|
||||
"temp_file_prefix": opts.TempFilePrefix,
|
||||
"client_mode": clientMode,
|
||||
"copied_bytes": fh.Size,
|
||||
"is_local": opts.IsLocalTempFile(),
|
||||
"is_multipart": opts.IsMultipart(),
|
||||
"is_remote": !opts.IsLocalTempFile(),
|
||||
"remote_id": opts.RemoteID,
|
||||
"client_mode": clientMode,
|
||||
"filename": fh.Name,
|
||||
})
|
||||
|
||||
if opts.IsLocalTempFile() {
|
||||
|
@ -226,7 +226,7 @@ func (fh *FileHandler) newLocalFile(ctx context.Context, opts *UploadOpts) (cons
|
|||
return nil, fmt.Errorf("newLocalFile: mkdir %q: %v", opts.LocalTempPath, err)
|
||||
}
|
||||
|
||||
file, err := ioutil.TempFile(opts.LocalTempPath, opts.TempFilePrefix)
|
||||
file, err := ioutil.TempFile(opts.LocalTempPath, "gitlab-workhorse-upload")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newLocalFile: create file: %v", err)
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@ func TestUploadWrongSize(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpFolder)
|
||||
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder, TempFilePrefix: "test-file"}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize+1, opts)
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize+1, "upload", opts)
|
||||
require.Error(t, err)
|
||||
_, isSizeError := err.(destination.SizeError)
|
||||
require.True(t, isSizeError, "Should fail with SizeError")
|
||||
|
@ -63,8 +63,8 @@ func TestUploadWithKnownSizeExceedLimit(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpFolder)
|
||||
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder, TempFilePrefix: "test-file", MaximumSize: test.ObjectSize - 1}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, opts)
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
|
||||
require.Error(t, err)
|
||||
_, isSizeError := err.(destination.SizeError)
|
||||
require.True(t, isSizeError, "Should fail with SizeError")
|
||||
|
@ -79,8 +79,8 @@ func TestUploadWithUnknownSizeExceedLimit(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpFolder)
|
||||
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder, TempFilePrefix: "test-file", MaximumSize: test.ObjectSize - 1}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), -1, opts)
|
||||
opts := &destination.UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), -1, "upload", opts)
|
||||
require.Equal(t, err, destination.ErrEntityTooLarge)
|
||||
require.Nil(t, fh)
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func TestUploadWrongETag(t *testing.T) {
|
|||
osStub.InitiateMultipartUpload(test.ObjectPath)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, opts)
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
|
||||
require.Nil(t, fh)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, osStub.PutsCnt(), "File not uploaded")
|
||||
|
@ -191,13 +191,12 @@ func TestUpload(t *testing.T) {
|
|||
|
||||
if spec.local {
|
||||
opts.LocalTempPath = tmpFolder
|
||||
opts.TempFilePrefix = "test-file"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, &opts)
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fh)
|
||||
|
||||
|
@ -211,9 +210,6 @@ func TestUpload(t *testing.T) {
|
|||
|
||||
dir := path.Dir(fh.LocalPath)
|
||||
require.Equal(t, opts.LocalTempPath, dir)
|
||||
filename := path.Base(fh.LocalPath)
|
||||
beginsWithPrefix := strings.HasPrefix(filename, opts.TempFilePrefix)
|
||||
require.True(t, beginsWithPrefix, fmt.Sprintf("LocalPath filename %q do not begin with TempFilePrefix %q", filename, opts.TempFilePrefix))
|
||||
} else {
|
||||
require.Empty(t, fh.LocalPath, "LocalPath must be empty for non local uploads")
|
||||
}
|
||||
|
@ -291,7 +287,7 @@ func TestUploadWithS3WorkhorseClient(t *testing.T) {
|
|||
MaximumSize: tc.maxSize,
|
||||
}
|
||||
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), tc.objectSize, &opts)
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), tc.objectSize, "upload", &opts)
|
||||
|
||||
if tc.expectedErr == nil {
|
||||
require.NoError(t, err)
|
||||
|
@ -324,7 +320,7 @@ func TestUploadWithAzureWorkhorseClient(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, &opts)
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
test.GoCloudObjectExists(t, bucketDir, remoteObject)
|
||||
|
@ -349,7 +345,7 @@ func TestUploadWithUnknownGoCloudScheme(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, &opts)
|
||||
_, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -375,7 +371,7 @@ func TestUploadMultipartInBodyFailure(t *testing.T) {
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, &opts)
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
|
||||
require.Nil(t, fh)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, test.MultipartUploadInternalError().Error())
|
||||
|
@ -468,7 +464,7 @@ func TestUploadRemoteFileWithLimit(t *testing.T) {
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(tc.testData), tc.objectSize, &opts)
|
||||
fh, err := destination.Upload(ctx, strings.NewReader(tc.testData), tc.objectSize, "upload", &opts)
|
||||
|
||||
if tc.expectedErr == nil {
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -22,7 +22,8 @@ type partsEtagMap map[int]string
|
|||
// Instead of storing objects it will just save md5sum.
|
||||
type ObjectstoreStub struct {
|
||||
// bucket contains md5sum of uploaded objects
|
||||
bucket map[string]string
|
||||
bucket map[string]string
|
||||
contents map[string][]byte
|
||||
// overwriteMD5 contains overwrites for md5sum that should be return instead of the regular hash
|
||||
overwriteMD5 map[string]string
|
||||
// multipart is a map of MultipartUploads
|
||||
|
@ -48,6 +49,7 @@ func StartObjectStoreWithCustomMD5(md5Hashes map[string]string) (*ObjectstoreStu
|
|||
multipart: make(map[string]partsEtagMap),
|
||||
overwriteMD5: make(map[string]string),
|
||||
headers: make(map[string]*http.Header),
|
||||
contents: make(map[string][]byte),
|
||||
}
|
||||
|
||||
for k, v := range md5Hashes {
|
||||
|
@ -82,6 +84,15 @@ func (o *ObjectstoreStub) GetObjectMD5(path string) string {
|
|||
return o.bucket[path]
|
||||
}
|
||||
|
||||
// GetObject returns the contents of the uploaded object. The caller must
|
||||
// not modify the byte slice.
|
||||
func (o *ObjectstoreStub) GetObject(path string) []byte {
|
||||
o.m.Lock()
|
||||
defer o.m.Unlock()
|
||||
|
||||
return o.contents[path]
|
||||
}
|
||||
|
||||
// GetHeader returns a given HTTP header of the object uploaded to the path
|
||||
func (o *ObjectstoreStub) GetHeader(path, key string) string {
|
||||
o.m.Lock()
|
||||
|
@ -154,11 +165,11 @@ func (o *ObjectstoreStub) putObject(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
etag, overwritten := o.overwriteMD5[objectPath]
|
||||
if !overwritten {
|
||||
buf, _ := io.ReadAll(r.Body)
|
||||
o.contents[objectPath] = buf
|
||||
hasher := md5.New()
|
||||
io.Copy(hasher, r.Body)
|
||||
|
||||
checksum := hasher.Sum(nil)
|
||||
etag = hex.EncodeToString(checksum)
|
||||
hasher.Write(buf)
|
||||
etag = hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
o.headers[objectPath] = &r.Header
|
||||
|
|
|
@ -29,8 +29,6 @@ type ObjectStorageConfig struct {
|
|||
|
||||
// UploadOpts represents all the options available for saving a file to object store
|
||||
type UploadOpts struct {
|
||||
// TempFilePrefix is the prefix used to create temporary local file
|
||||
TempFilePrefix string
|
||||
// LocalTempPath is the directory where to write a local copy of the file
|
||||
LocalTempPath string
|
||||
// RemoteID is the remote ObjectID provided by GitLab
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package upload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
)
|
||||
|
||||
// Multipart is a request middleware. If the request has a MIME multipart
|
||||
|
@ -17,12 +15,19 @@ func Multipart(rails PreAuthorizer, h http.Handler, p Preparer) http.Handler {
|
|||
return rails.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
|
||||
s := &SavedFileTracker{Request: r}
|
||||
|
||||
opts, err := p.Prepare(a)
|
||||
if err != nil {
|
||||
helper.Fail500(w, r, fmt.Errorf("Multipart: error preparing file storage options"))
|
||||
return
|
||||
}
|
||||
|
||||
interceptMultipartFiles(w, r, h, a, s, opts)
|
||||
interceptMultipartFiles(w, r, h, s, &eagerAuthorizer{a}, p)
|
||||
}, "/authorize")
|
||||
}
|
||||
|
||||
// SkipRailsPreAuthMultipart behaves like Multipart except it does not
|
||||
// pre-authorize with Rails. It is intended for use on catch-all routes
|
||||
// where we cannot pre-authorize both because we don't know which Rails
|
||||
// endpoint to call, and because eagerly pre-authorizing would add too
|
||||
// much overhead.
|
||||
func SkipRailsPreAuthMultipart(tempPath string, h http.Handler, p Preparer) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s := &SavedFileTracker{Request: r}
|
||||
fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
|
||||
interceptMultipartFiles(w, r, h, s, fa, p)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -62,13 +62,14 @@ var (
|
|||
)
|
||||
|
||||
type rewriter struct {
|
||||
writer *multipart.Writer
|
||||
preauth *api.Response
|
||||
writer *multipart.Writer
|
||||
fileAuthorizer
|
||||
Preparer
|
||||
filter MultipartFormProcessor
|
||||
finalizedFields map[string]bool
|
||||
}
|
||||
|
||||
func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, preauth *api.Response, filter MultipartFormProcessor, opts *destination.UploadOpts) error {
|
||||
func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, filter MultipartFormProcessor, fa fileAuthorizer, preparer Preparer) error {
|
||||
// Create multipart reader
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
|
@ -83,7 +84,8 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
|
|||
|
||||
rew := &rewriter{
|
||||
writer: writer,
|
||||
preauth: preauth,
|
||||
fileAuthorizer: fa,
|
||||
Preparer: preparer,
|
||||
filter: filter,
|
||||
finalizedFields: make(map[string]bool),
|
||||
}
|
||||
|
@ -108,7 +110,7 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
|
|||
}
|
||||
|
||||
if filename != "" {
|
||||
err = rew.handleFilePart(r.Context(), name, p, opts)
|
||||
err = rew.handleFilePart(r, name, p)
|
||||
} else {
|
||||
err = rew.copyPart(r.Context(), name, p)
|
||||
}
|
||||
|
@ -128,7 +130,7 @@ func parseAndNormalizeContentDisposition(header textproto.MIMEHeader) (string, s
|
|||
return params["name"], params["filename"]
|
||||
}
|
||||
|
||||
func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipart.Part, opts *destination.UploadOpts) error {
|
||||
func (rew *rewriter) handleFilePart(r *http.Request, name string, p *multipart.Part) error {
|
||||
if rew.filter.Count() >= maxFilesAllowed {
|
||||
return ErrTooManyFilesUploaded
|
||||
}
|
||||
|
@ -141,30 +143,34 @@ func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipa
|
|||
return fmt.Errorf("illegal filename: %q", filename)
|
||||
}
|
||||
|
||||
opts.TempFilePrefix = filename
|
||||
apiResponse, err := rew.AuthorizeFile(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts, err := rew.Prepare(apiResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var inputReader io.ReadCloser
|
||||
var err error
|
||||
|
||||
imageType := exif.FileTypeFromSuffix(filename)
|
||||
switch {
|
||||
case imageType != exif.TypeUnknown:
|
||||
ctx := r.Context()
|
||||
if imageType := exif.FileTypeFromSuffix(filename); imageType != exif.TypeUnknown {
|
||||
inputReader, err = handleExifUpload(ctx, p, filename, imageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case rew.preauth.ProcessLsif:
|
||||
inputReader, err = handleLsifUpload(ctx, p, opts.LocalTempPath, filename, rew.preauth)
|
||||
} else if apiResponse.ProcessLsif {
|
||||
inputReader, err = handleLsifUpload(ctx, p, opts.LocalTempPath, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
inputReader = ioutil.NopCloser(p)
|
||||
}
|
||||
|
||||
defer inputReader.Close()
|
||||
|
||||
fh, err := destination.Upload(ctx, inputReader, -1, opts)
|
||||
fh, err := destination.Upload(ctx, inputReader, -1, filename, opts)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case destination.ErrEntityTooLarge, exif.ErrRemovingExif:
|
||||
|
@ -267,7 +273,7 @@ func isJPEG(r io.Reader) bool {
|
|||
return http.DetectContentType(buf) == "image/jpeg"
|
||||
}
|
||||
|
||||
func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string, preauth *api.Response) (io.ReadCloser, error) {
|
||||
func handleLsifUpload(ctx context.Context, reader io.Reader, tempPath, filename string) (io.ReadCloser, error) {
|
||||
parserConfig := parser.Config{
|
||||
TempPath: tempPath,
|
||||
}
|
||||
|
@ -291,3 +297,15 @@ func (rew *rewriter) copyPart(ctx context.Context, name string, p *multipart.Par
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileAuthorizer interface {
|
||||
AuthorizeFile(*http.Request) (*api.Response, error)
|
||||
}
|
||||
|
||||
type eagerAuthorizer struct{ response *api.Response }
|
||||
|
||||
func (ea *eagerAuthorizer) AuthorizeFile(r *http.Request) (*api.Response, error) {
|
||||
return ea.response, nil
|
||||
}
|
||||
|
||||
var _ fileAuthorizer = &eagerAuthorizer{}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
package upload
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
||||
)
|
||||
|
||||
// SkipRailsAuthorizer implements a fake PreAuthorizer that does not call
|
||||
// the gitlab-rails API. It must be fast because it gets called on each
|
||||
// request proxied to Rails.
|
||||
type SkipRailsAuthorizer struct {
|
||||
// TempPath is a directory where workhorse can store files that can later
|
||||
// be accessed by gitlab-rails.
|
||||
TempPath string
|
||||
}
|
||||
|
||||
func (l *SkipRailsAuthorizer) PreAuthorizeHandler(next api.HandleFunc, _ string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next(w, r, &api.Response{TempPath: l.TempPath})
|
||||
})
|
||||
}
|
|
@ -40,13 +40,13 @@ type MultipartFormProcessor interface {
|
|||
|
||||
// interceptMultipartFiles is the core of the implementation of
|
||||
// Multipart.
|
||||
func interceptMultipartFiles(w http.ResponseWriter, r *http.Request, h http.Handler, preauth *api.Response, filter MultipartFormProcessor, opts *destination.UploadOpts) {
|
||||
func interceptMultipartFiles(w http.ResponseWriter, r *http.Request, h http.Handler, filter MultipartFormProcessor, fa fileAuthorizer, p Preparer) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
defer writer.Close()
|
||||
|
||||
// Rewrite multipart form data
|
||||
err := rewriteFormFilesFromMultipart(r, writer, preauth, filter, opts)
|
||||
err := rewriteFormFilesFromMultipart(r, writer, filter, fa, p)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrInjectedClientParam:
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -66,19 +67,14 @@ func TestUploadHandlerForwardingRawData(t *testing.T) {
|
|||
httpRequest, err := http.NewRequest("PATCH", ts.URL+"/url/path", bytes.NewBufferString("REQUEST"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
tempPath := t.TempDir()
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
handler := newProxy(ts.URL)
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, handler, apiResponse, nil, opts)
|
||||
interceptMultipartFiles(response, httpRequest, handler, nil, fa, preparer)
|
||||
|
||||
require.Equal(t, 202, response.Code)
|
||||
require.Equal(t, "RESPONSE", response.Body.String(), "response body")
|
||||
|
@ -86,10 +82,7 @@ func TestUploadHandlerForwardingRawData(t *testing.T) {
|
|||
|
||||
func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
|
||||
var filePath string
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
tempPath := t.TempDir()
|
||||
|
||||
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`/url/path\z`), func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "PUT", r.Method, "method")
|
||||
|
@ -144,12 +137,10 @@ func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
|
|||
|
||||
handler := newProxy(ts.URL)
|
||||
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
fa := &eagerAuthorizer{&api.Response{TempPath: tempPath}}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts)
|
||||
interceptMultipartFiles(response, httpRequest, handler, &testFormProcessor{}, fa, preparer)
|
||||
require.Equal(t, 202, response.Code)
|
||||
|
||||
cancel() // this will trigger an async cleanup
|
||||
|
@ -159,10 +150,6 @@ func TestUploadHandlerRewritingMultiPartData(t *testing.T) {
|
|||
func TestUploadHandlerDetectingInjectedMultiPartData(t *testing.T) {
|
||||
var filePath string
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
|
@ -213,12 +200,8 @@ func TestUploadHandlerDetectingInjectedMultiPartData(t *testing.T) {
|
|||
response := httptest.NewRecorder()
|
||||
|
||||
handler := newProxy(ts.URL)
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, handler, &testFormProcessor{})
|
||||
require.Equal(t, test.response, response.Code)
|
||||
|
||||
cancel() // this will trigger an async cleanup
|
||||
|
@ -228,10 +211,6 @@ func TestUploadHandlerDetectingInjectedMultiPartData(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUploadProcessingField(t *testing.T) {
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
writer := multipart.NewWriter(&buffer)
|
||||
|
@ -243,12 +222,8 @@ func TestUploadProcessingField(t *testing.T) {
|
|||
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, nilHandler, apiResponse, &testFormProcessor{}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, nilHandler, &testFormProcessor{})
|
||||
|
||||
require.Equal(t, 500, response.Code)
|
||||
}
|
||||
|
@ -256,15 +231,11 @@ func TestUploadProcessingField(t *testing.T) {
|
|||
func TestUploadingMultipleFiles(t *testing.T) {
|
||||
testhelper.ConfigureSecret()
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
writer := multipart.NewWriter(&buffer)
|
||||
for i := 0; i < 11; i++ {
|
||||
_, err = writer.CreateFormFile(fmt.Sprintf("file %v", i), "my.file")
|
||||
_, err := writer.CreateFormFile(fmt.Sprintf("file %v", i), "my.file")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, writer.Close())
|
||||
|
@ -274,23 +245,18 @@ func TestUploadingMultipleFiles(t *testing.T) {
|
|||
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, nilHandler, apiResponse, &testFormProcessor{}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, nilHandler, &testFormProcessor{})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, "upload request contains more than 10 files\n", response.Body.String())
|
||||
}
|
||||
|
||||
func TestUploadProcessingFile(t *testing.T) {
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
testhelper.ConfigureSecret()
|
||||
tempPath := t.TempDir()
|
||||
|
||||
_, testServer := test.StartObjectStore()
|
||||
objectStore, testServer := test.StartObjectStore()
|
||||
defer testServer.Close()
|
||||
|
||||
storeUrl := testServer.URL + test.ObjectPath
|
||||
|
@ -298,21 +264,24 @@ func TestUploadProcessingFile(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
preauth *api.Response
|
||||
content func(t *testing.T) []byte
|
||||
}{
|
||||
{
|
||||
name: "FileStore Upload",
|
||||
preauth: &api.Response{TempPath: tempPath},
|
||||
content: func(t *testing.T) []byte {
|
||||
entries, err := os.ReadDir(tempPath)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 1)
|
||||
content, err := os.ReadFile(path.Join(tempPath, entries[0].Name()))
|
||||
require.NoError(t, err)
|
||||
return content
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ObjectStore Upload",
|
||||
preauth: &api.Response{RemoteObject: api.RemoteObject{StoreURL: storeUrl}},
|
||||
},
|
||||
{
|
||||
name: "ObjectStore and FileStore Upload",
|
||||
preauth: &api.Response{
|
||||
TempPath: tempPath,
|
||||
RemoteObject: api.RemoteObject{StoreURL: storeUrl},
|
||||
},
|
||||
preauth: &api.Response{RemoteObject: api.RemoteObject{StoreURL: storeUrl, ID: "123"}},
|
||||
content: func(*testing.T) []byte { return objectStore.GetObject(test.ObjectPath) },
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -330,26 +299,20 @@ func TestUploadProcessingFile(t *testing.T) {
|
|||
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
fa := &eagerAuthorizer{test.preauth}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, nilHandler, apiResponse, &testFormProcessor{}, opts)
|
||||
interceptMultipartFiles(response, httpRequest, nilHandler, &testFormProcessor{}, fa, preparer)
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, "test", string(test.content(t)))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInvalidFileNames(t *testing.T) {
|
||||
testhelper.ConfigureSecret()
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
for _, testCase := range []struct {
|
||||
filename string
|
||||
code int
|
||||
|
@ -376,24 +339,14 @@ func TestInvalidFileNames(t *testing.T) {
|
|||
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, nilHandler, apiResponse, &SavedFileTracker{Request: httpRequest}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, nilHandler, &SavedFileTracker{Request: httpRequest})
|
||||
require.Equal(t, testCase.code, response.Code)
|
||||
require.Equal(t, testCase.expectedPrefix, opts.TempFilePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentDispositionRewrite(t *testing.T) {
|
||||
testhelper.ConfigureSecret()
|
||||
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
header string
|
||||
|
@ -442,12 +395,7 @@ func TestContentDispositionRewrite(t *testing.T) {
|
|||
})
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, customHandler, apiResponse, &SavedFileTracker{Request: httpRequest}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, customHandler, &SavedFileTracker{Request: httpRequest})
|
||||
|
||||
upstreamRequest, err := http.ReadRequest(bufio.NewReader(&upstreamRequestBuffer))
|
||||
require.NoError(t, err)
|
||||
|
@ -534,10 +482,6 @@ func TestUploadHandlerRemovingExifCorruptedFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func runUploadTest(t *testing.T, image []byte, filename string, httpCode int, tsHandler func(http.ResponseWriter, *http.Request)) {
|
||||
tempPath, err := ioutil.TempDir("", "uploads")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempPath)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
writer := multipart.NewWriter(&buffer)
|
||||
|
@ -565,12 +509,8 @@ func runUploadTest(t *testing.T, image []byte, filename string, httpCode int, ts
|
|||
response := httptest.NewRecorder()
|
||||
|
||||
handler := newProxy(ts.URL)
|
||||
apiResponse := &api.Response{TempPath: tempPath}
|
||||
preparer := &DefaultPreparer{}
|
||||
opts, err := preparer.Prepare(apiResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
interceptMultipartFiles(response, httpRequest, handler, apiResponse, &testFormProcessor{}, opts)
|
||||
testInterceptMultipartFiles(t, response, httpRequest, handler, &testFormProcessor{})
|
||||
require.Equal(t, httpCode, response.Code)
|
||||
}
|
||||
|
||||
|
@ -587,3 +527,12 @@ func waitUntilDeleted(t *testing.T, path string) {
|
|||
}, 10*time.Second, 10*time.Millisecond)
|
||||
require.True(t, os.IsNotExist(err), "expected the file to be deleted")
|
||||
}
|
||||
|
||||
func testInterceptMultipartFiles(t *testing.T, w http.ResponseWriter, r *http.Request, h http.Handler, filter MultipartFormProcessor) {
|
||||
t.Helper()
|
||||
|
||||
fa := &eagerAuthorizer{&api.Response{TempPath: t.TempDir()}}
|
||||
preparer := &DefaultPreparer{}
|
||||
|
||||
interceptMultipartFiles(w, r, h, filter, fa, preparer)
|
||||
}
|
||||
|
|
|
@ -223,7 +223,7 @@ func configureRoutes(u *upstream) {
|
|||
mimeMultipartUploader := upload.Multipart(api, signingProxy, preparer)
|
||||
|
||||
uploadPath := path.Join(u.DocumentRoot, "uploads/tmp")
|
||||
tempfileMultipartProxy := upload.Multipart(&upload.SkipRailsAuthorizer{TempPath: uploadPath}, proxy, preparer)
|
||||
tempfileMultipartProxy := upload.SkipRailsPreAuthMultipart(uploadPath, proxy, preparer)
|
||||
ciAPIProxyQueue := queueing.QueueRequests("ci_api_job_requests", tempfileMultipartProxy, u.APILimit, u.APIQueueLimit, u.APIQueueTimeout)
|
||||
ciAPILongPolling := builds.RegisterHandler(ciAPIProxyQueue, redis.WatchKey, u.APICILongPollingDuration)
|
||||
|
||||
|
|
Loading…
Reference in New Issue