Commit 273e1029 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into 'ee-url-utility-es-module'

# Conflicts:
#   app/assets/javascripts/issue_show/components/app.vue
parents 37946c88 bbcaf4ae
...@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1' ...@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# Cache # Cache
gem 'redis-rails', '~> 5.0.1' gem 'redis-rails', '~> 5.0.2'
# Redis # Redis
gem 'redis', '~> 3.2' gem 'redis', '~> 3.2'
......
...@@ -728,24 +728,24 @@ GEM ...@@ -728,24 +728,24 @@ GEM
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.3) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.1) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (1.6.0) redis-rack (2.0.3)
rack (~> 1.5) rack (>= 1.5, < 3)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-rails (5.0.1) redis-rails (5.0.2)
redis-actionpack (~> 5.0.0) redis-actionpack (>= 5.0, < 6)
redis-activesupport (~> 5.0.0) redis-activesupport (>= 5.0, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-store (1.2.0) redis-store (1.4.1)
redis (>= 2.2) redis (>= 2.2, < 5)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
...@@ -1168,7 +1168,7 @@ DEPENDENCIES ...@@ -1168,7 +1168,7 @@ DEPENDENCIES
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
window.Compare = (function() { export default class Compare {
function Compare(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
this.source_loading = $(".js-source-loading"); this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading"); this.target_loading = $(".js-target-loading");
...@@ -34,12 +34,12 @@ window.Compare = (function() { ...@@ -34,12 +34,12 @@ window.Compare = (function() {
this.initialState(); this.initialState();
} }
Compare.prototype.initialState = function() { initialState() {
this.getSourceHtml(); this.getSourceHtml();
return this.getTargetHtml(); this.getTargetHtml();
}; }
Compare.prototype.getTargetProject = function() { getTargetProject() {
return $.ajax({ return $.ajax({
url: this.opts.targetProjectUrl, url: this.opts.targetProjectUrl,
data: { data: {
...@@ -52,22 +52,22 @@ window.Compare = (function() { ...@@ -52,22 +52,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html); return $('.js-target-branch-dropdown .dropdown-content').html(html);
} }
}); });
}; }
Compare.prototype.getSourceHtml = function() { getSourceHtml() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val() ref: $("input[name='merge_request[source_branch]']").val()
}); });
}; }
Compare.prototype.getTargetHtml = function() { getTargetHtml() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(), target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val() ref: $("input[name='merge_request[target_branch]']").val()
}); });
}; }
Compare.prototype.sendAjax = function(url, loading, target, data) { static sendAjax(url, loading, target, data) {
var $target; var $target;
$target = $(target); $target = $(target);
return $.ajax({ return $.ajax({
...@@ -84,7 +84,5 @@ window.Compare = (function() { ...@@ -84,7 +84,5 @@ window.Compare = (function() {
gl.utils.localTimeAgo($('.js-timeago', className)); gl.utils.localTimeAgo($('.js-timeago', className));
} }
}); });
}; }
}
return Compare;
})();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
window.CompareAutocomplete = (function() { export default function initCompareAutocomplete() {
function CompareAutocomplete() { $('.js-compare-dropdown').each(function() {
this.initDropdown(); var $dropdown, selected;
} $dropdown = $(this);
selected = $dropdown.data('selected');
CompareAutocomplete.prototype.initDropdown = function() { const $dropdownContainer = $dropdown.closest('.dropdown');
return $('.js-compare-dropdown').each(function() { const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
var $dropdown, selected; const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown = $(this); $dropdown.glDropdown({
selected = $dropdown.data('selected'); data: function(term, callback) {
const $dropdownContainer = $dropdown.closest('.dropdown'); return $.ajax({
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); url: $dropdown.data('refs-url'),
const $filterInput = $('input[type="search"]', $dropdownContainer); data: {
$dropdown.glDropdown({ ref: $dropdown.data('ref'),
data: function(term, callback) { search: term,
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
}
}).done(function(refs) {
return callback(refs);
});
},
selectable: true,
filterable: true,
filterRemote: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
return $('<li />').append(link);
} }
}, }).done(function(refs) {
id: function(obj, $el) { return callback(refs);
return $el.attr('data-ref'); });
}, },
toggleLabel: function(obj, $el) { selectable: true,
return $el.text().trim(); filterable: true,
} filterRemote: true,
}); fieldName: $dropdown.data('field-name'),
$filterInput.on('keyup', (e) => { filterInput: 'input[type="search"]',
const keyCode = e.keyCode || e.which; renderRow: function(ref) {
if (keyCode !== 13) return; var link;
const text = $filterInput.val(); if (ref.header != null) {
$fieldInput.val(text); return $('<li />').addClass('dropdown-header').text(ref.header);
$('.dropdown-toggle-text', $dropdown).text(text); } else {
$dropdownContainer.removeClass('open'); link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
}); return $('<li />').append(link);
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
} }
}); },
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
}
});
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $dropdown).text(text);
$dropdownContainer.removeClass('open');
}); });
};
return CompareAutocomplete; $dropdownContainer.on('click', '.dropdown-content a', (e) => {
})(); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
}
});
});
}
...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form'; ...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form';
import Project from './project'; import Project from './project';
import projectAvatar from './project_avatar'; import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ import Compare from './compare';
/* global CompareAutocomplete */ import initCompareAutocomplete from './compare_autocomplete';
/* global PathLocks */ /* global PathLocks */
/* global ProjectFindFile */ /* global ProjectFindFile */
import ProjectNew from './project_new'; import ProjectNew from './project_new';
...@@ -711,7 +711,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -711,7 +711,7 @@ import initGroupAnalytics from './init_group_analytics';
projectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); initCompareAutocomplete();
break; break;
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue { export default class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) { if ($('a.btn-close').length) this.initIssueBtnEventListeners();
this.taskList = new TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: (result) => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners();
}
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
......
...@@ -9,6 +9,7 @@ import titleComponent from './title.vue'; ...@@ -9,6 +9,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue'; import descriptionComponent from './description.vue';
import editedComponent from './edited.vue'; import editedComponent from './edited.vue';
import formComponent from './form.vue'; import formComponent from './form.vue';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
props: { props: {
...@@ -149,6 +150,11 @@ export default { ...@@ -149,6 +150,11 @@ export default {
editedComponent, editedComponent,
formComponent, formComponent,
}, },
mixins: [
RecaptchaDialogImplementor,
],
methods: { methods: {
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
...@@ -164,9 +170,11 @@ export default { ...@@ -164,9 +170,11 @@ export default {
closeForm() { closeForm() {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { updateIssuable() {
this.service.updateIssuable(this.store.formState) this.service.updateIssuable(this.store.formState)
.then(res => res.json()) .then(res => res.json())
.then(data => this.checkForSpam(data))
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
urlUtils.visitUrl(data.web_url); urlUtils.visitUrl(data.web_url);
...@@ -179,11 +187,24 @@ export default { ...@@ -179,11 +187,24 @@ export default {
this.store.updateState(data); this.store.updateState(data);
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}) })
.catch(() => { .catch((error) => {
eventHub.$emit('close.form'); if (error && error.name === 'SpamError') {
window.Flash(`Error updating ${this.issuableType}`); this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
}); });
}, },
closeRecaptchaDialog() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() { deleteIssuable() {
this.service.deleteIssuable() this.service.deleteIssuable()
.then(res => res.json()) .then(res => res.json())
...@@ -237,9 +258,9 @@ export default { ...@@ -237,9 +258,9 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div v-if="canUpdate && showForm">
<form-component <form-component
v-if="canUpdate && showForm"
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" :can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
...@@ -250,30 +271,37 @@ export default { ...@@ -250,30 +271,37 @@ export default {
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<div v-else>
<title-component <recaptcha-dialog
:issuable-ref="issuableRef" v-show="showRecaptcha"
:can-update="canUpdate" :html="recaptchaHTML"
:title-html="state.titleHtml" @close="closeRecaptchaDialog"
:title-text="state.titleText" />
:show-inline-edit-button="showInlineEditButton" </div>
/> <div v-else>
<description-component <title-component
v-if="state.descriptionHtml" :issuable-ref="issuableRef"
:can-update="canUpdate" :can-update="canUpdate"
:description-html="state.descriptionHtml" :title-html="state.titleHtml"
:description-text="state.descriptionText" :title-text="state.titleText"
:updated-at="state.updatedAt" :show-inline-edit-button="showInlineEditButton"
:task-status="state.taskStatus" />
:issuable-type="issuableType" <description-component
:update-url="updateEndpoint" v-if="state.descriptionHtml"
/> :can-update="canUpdate"
<edited-component :description-html="state.descriptionHtml"
v-if="hasUpdated" :description-text="state.descriptionText"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:updated-by-name="state.updatedByName" :task-status="state.taskStatus"
:updated-by-path="state.updatedByPath" :issuable-type="issuableType"
/> :update-url="updateEndpoint"
</div> />
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath"
/>
</div> </div>
</div>
</template> </template>
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default { export default {
mixins: [animateMixin], mixins: [
animateMixin,
RecaptchaDialogImplementor,
],
props: { props: {
canUpdate: { canUpdate: {
type: Boolean, type: Boolean,
...@@ -51,6 +56,7 @@ ...@@ -51,6 +56,7 @@
this.updateTaskStatusText(); this.updateTaskStatusText();
}, },
}, },
methods: { methods: {
renderGFM() { renderGFM() {
$(this.$refs['gfm-content']).renderGFM(); $(this.$refs['gfm-content']).renderGFM();
...@@ -61,9 +67,19 @@ ...@@ -61,9 +67,19 @@
dataType: this.issuableType, dataType: this.issuableType,
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
}); });
} }
}, },
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() { updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta'); const $issuableHeader = $('.issuable-meta');
...@@ -109,5 +125,11 @@ ...@@ -109,5 +125,11 @@
:data-update-url="updateUrl" :data-update-url="updateUrl"
> >
</textarea> </textarea>
<recaptcha-dialog
v-show="showRecaptcha"
:html="recaptchaHTML"
@close="closeRecaptcha"
/>
</div> </div>
</template> </template>
...@@ -40,9 +40,6 @@ import './admin'; ...@@ -40,9 +40,6 @@ import './admin';
import './aside'; import './aside';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
......
...@@ -38,7 +38,8 @@ export default { ...@@ -38,7 +38,8 @@ export default {
}, },
primaryButtonLabel: { primaryButtonLabel: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
submitDisabled: { submitDisabled: {
type: Boolean, type: Boolean,
...@@ -113,8 +114,9 @@ export default { ...@@ -113,8 +114,9 @@ export default {
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<button <button
v-if="primaryButtonLabel"
type="button" type="button"
class="btn pull-right" class="btn pull-right js-primary-button"
:disabled="submitDisabled" :disabled="submitDisabled"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit(true)">
......
<script>
import PopupDialog from './popup_dialog.vue';
export default {
name: 'recaptcha-dialog',
props: {
html: {
type: String,
required: false,
default: '',
},
},
data() {
return {
script: {},
scriptSrc: 'https://www.google.com/recaptcha/api.js',
};
},
components: {
PopupDialog,
},
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
this.script = script;
document.body.appendChild(script);
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
submit() {
this.$el.querySelector('form').submit();
},
},
watch: {
html() {
this.appendRecaptchaScript();
},
},
mounted() {
window.recaptchaDialogCallback = this.submit.bind(this);
},
};
</script>
<template>
<popup-dialog
kind="warning"
class="recaptcha-dialog js-recaptcha-dialog"
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
@toggle="close"
>
<div slot="body">
<p>
{{__('We want to be sure it is you, please confirm you are not a robot.')}}
</p>
<div
ref="recaptcha"
v-html="html"
></div>
</div>
</popup-dialog>
</template>
import RecaptchaDialog from '../components/recaptcha_dialog.vue';
export default {
data() {
return {
showRecaptcha: false,
recaptchaHTML: '',
};
},
components: {
RecaptchaDialog,
},
methods: {
openRecaptcha() {
this.showRecaptcha = true;
},
closeRecaptcha() {
this.showRecaptcha = false;
},
checkForSpam(data) {
if (!data.recaptcha_html) return data;
this.recaptchaHTML = data.recaptcha_html;
const spamError = new Error(data.error_message);
spamError.name = 'SpamError';
spamError.message = 'SpamError';
throw spamError;
},
},
};
...@@ -47,3 +47,11 @@ body.modal-open { ...@@ -47,3 +47,11 @@ body.modal-open {
.modal.popup-dialog { .modal.popup-dialog {
display: block; display: block;
} }
.recaptcha-dialog .recaptcha-form {
display: inline-block;
.recaptcha {
margin: 0;
}
}
...@@ -21,11 +21,11 @@ module IssuableActions ...@@ -21,11 +21,11 @@ module IssuableActions
respond_to do |format| respond_to do |format|
format.html do format.html do
recaptcha_check_with_fallback { render :edit } recaptcha_check_if_spammable { render :edit }
end end
format.json do format.json do
render_entity_json recaptcha_check_if_spammable(false) { render_entity_json }
end end
end end
...@@ -80,6 +80,12 @@ module IssuableActions ...@@ -80,6 +80,12 @@ module IssuableActions
private private
def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless @issuable.is_a? Spammable
recaptcha_check_with_fallback(should_redirect, &block)
end
def render_conflict_response def render_conflict_response
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -23,8 +23,8 @@ module SpammableActions ...@@ -23,8 +23,8 @@ module SpammableActions
@spam_config_loaded = Gitlab::Recaptcha.load_configurations! @spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end end
def recaptcha_check_with_fallback(&fallback) def recaptcha_check_with_fallback(should_redirect = true, &fallback)
if spammable.valid? if should_redirect && spammable.valid?
redirect_to spammable_path redirect_to spammable_path
elsif render_recaptcha? elsif render_recaptcha?
ensure_spam_config_loaded! ensure_spam_config_loaded!
...@@ -33,7 +33,18 @@ module SpammableActions ...@@ -33,7 +33,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end end
render :verify respond_to do |format|
format.html do
render :verify
end
format.json do
locals = { spammable: spammable, script: false, has_submit: false }
recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
render json: { recaptcha_html: recaptcha_html }
end
end
else else
yield yield
end end
......
...@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base ...@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true namespace_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base ...@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id)) Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true) .update_all(share_with_group_lock: true)
end end
def allowed_path_by_redirects
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
end
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
end end
...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base ...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%") where(wheres, path, "#{sanitize_sql_like(path)}/%")
end end
scope :permanent, -> do
if column_permanent_exists?
where(permanent: true)
else
none
end
end
scope :temporary, -> do
if column_permanent_exists?
where(permanent: [false, nil])
else
all
end
end
default_value_for :permanent, false
def permanent=(value)
if self.class.column_permanent_exists?
super
end
end
def self.column_permanent_exists?
ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
end
end end
...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base ...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path after_update :create_redirect_for_old_path
...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base ...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes # We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already # of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants. # called it, which deletes conflicts for all descendants.
route.create_redirect(old_path) if attributes[:path] route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end end
end end
end end
...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base ...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end end
def conflicting_redirects def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path) RedirectRoute.temporary.matching_path_and_descendants(path)
end end
def create_redirect(path) def create_redirect(path, permanent: false)
RedirectRoute.create(source: source, path: path) RedirectRoute.create(source: source, path: path, permanent: permanent)
end end
private private
def create_redirect_for_old_path def create_redirect_for_old_path
create_redirect(path_was) if path_changed? create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
end
def permanent_redirect?
source_type != "Project"
end
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
end end
...@@ -14,19 +14,23 @@ module Ci ...@@ -14,19 +14,23 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new @pipeline = Ci::Pipeline.new
command = OpenStruct.new(source: source, command = Gitlab::Ci::Pipeline::Chain::Command.new(
origin_ref: params[:ref], source: source,
checkout_sha: params[:checkout_sha], origin_ref: params[:ref],
after_sha: params[:after], checkout_sha: params[:checkout_sha],
before_sha: params[:before], after_sha: params[:after],
trigger_request: trigger_request, before_sha: params[:before],
schedule: schedule, trigger_request: trigger_request,
ignore_skip_ci: ignore_skip_ci, schedule: schedule,
save_incompleted: save_on_errors, ignore_skip_ci: ignore_skip_ci,
allow_mirror_update: mirror_update, save_incompleted: save_on_errors,
seeds_block: block, seeds_block: block,
project: project, project: project,
current_user: current_user) current_user: current_user,
# EE specific
allow_mirror_update: mirror_update
)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE) .new(pipeline, command, SEQUENCE)
......
- humanized_resource_name = spammable.class.model_name.human.downcase - humanized_resource_name = spammable.class.model_name.human.downcase
- resource_name = spammable.class.model_name.singular
%h3.page-title %h3.page-title
Anti-spam verification Anti-spam verification
...@@ -8,16 +7,4 @@ ...@@ -8,16 +7,4 @@
%p %p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."} #{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
= form_for form do |f| = render 'shared/recaptcha_form', spammable: spammable
.recaptcha
- params[resource_name].each do |field, value|
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
-# Yields a block with given extra params.
= yield
.row-content-block.footer-block
= f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
- resource_name = spammable.class.model_name.singular
- humanized_resource_name = spammable.class.model_name.human.downcase
- script = local_assigns.fetch(:script, true)
- has_submit = local_assigns.fetch(:has_submit, true)
= form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
.recaptcha
- params[resource_name].each do |field, value|
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags script: script, callback: 'recaptchaDialogCallback'
-# Yields a block with given extra params.
= yield
- if has_submit
.row-content-block.footer-block
= f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
---
title: Document how to set up GitLab Geo for HA
merge_request: 3468
author:
type: other
---
title: Add recaptcha modal to issue updates detected as spam
merge_request: 15408
author:
type: fixed
---
title: Allow git pull/push on group/user/project redirects
merge_request: 15670
author:
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPermanentToRedirectRoute < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
add_column(:redirect_routes, :permanent, :boolean)
end
def down
remove_column(:redirect_routes, :permanent)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddPermanentIndexToRedirectRoute < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:redirect_routes, :permanent)
end
def down
remove_concurrent_index(:redirect_routes, :permanent) if index_exists?(:redirect_routes, :permanent)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171205190711) do ActiveRecord::Schema.define(version: 20171206221519) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1972,10 +1972,12 @@ ActiveRecord::Schema.define(version: 20171205190711) do ...@@ -1972,10 +1972,12 @@ ActiveRecord::Schema.define(version: 20171205190711) do
t.string "path", null: false t.string "path", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "permanent"
end end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"} add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t| create_table "releases", force: :cascade do |t|
......
...@@ -181,6 +181,10 @@ Read through the [GitLab Geo configuration](configuration.md) documentation. ...@@ -181,6 +181,10 @@ Read through the [GitLab Geo configuration](configuration.md) documentation.
Read how to [update your Geo nodes to the latest GitLab version](updating_the_geo_nodes.md). Read how to [update your Geo nodes to the latest GitLab version](updating_the_geo_nodes.md).
## Configuring GitLab Geo HA
Read through the [Geo High Availability documentation](ha.md).
## Current limitations ## Current limitations
- You cannot push code to secondary nodes - You cannot push code to secondary nodes
......
...@@ -12,11 +12,9 @@ This is the final step in setting up a secondary Geo node. Stages of the ...@@ -12,11 +12,9 @@ This is the final step in setting up a secondary Geo node. Stages of the
setup process must be completed in the documented order. setup process must be completed in the documented order.
Before attempting the steps in this stage, [complete all prior stages](README.md#using-omnibus-gitlab). Before attempting the steps in this stage, [complete all prior stages](README.md#using-omnibus-gitlab).
The basic steps of configuring a secondary node are: The basic steps of configuring a secondary node are to replicate required
configurations between the primary and the secondaries; to configure a tracking
1. replicate required configurations between the primary and the secondaries; database on each secondary; and to start GitLab on the secondary node.
1. configure a second, tracking database on each secondary;
1. start GitLab on the secondary node's machine.
You are encouraged to first read through all the steps before executing them You are encouraged to first read through all the steps before executing them
in your testing/production environment. in your testing/production environment.
...@@ -99,35 +97,30 @@ on the secondary. ...@@ -99,35 +97,30 @@ on the secondary.
### Step 4. Enable Git access over HTTP/HTTPS ### Step 4. Enable Git access over HTTP/HTTPS
GitLab Geo synchronizes repositories over HTTP/HTTPS, and so requires this clone GitLab Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
method to be enabled. Navigate to **Admin Area ➔ Settings** method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set (`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`. `Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Verify proper functioning of the secondary node ### Step 5. Verify proper functioning of the secondary node
Your nodes should now be ready to use. You can login to the secondary node
with the same credentials as used in the primary. Visit the secondary node's
**Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in your browser to check if it's
correctly identified as a secondary Geo node and if Geo is enabled.
If your installation isn't working properly, check the
[troubleshooting document](troubleshooting.md).
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md). Congratulations! Your secondary geo node is now configured!
You can monitor the status of the syncing process on a secondary node You can login to the secondary node with the same credentials you used on the
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) primary. Visit the secondary node's **Admin Area ➔ Geo Nodes**
in your browser. (`/admin/geo_nodes`) in your browser to check if it's correctly identified as a
secondary Geo node and if Geo is enabled.
Please note that if `git_data_dirs` is customized on the primary for multiple The initial replication, or 'backfill', will probably still be in progress. You
repository shards you must duplicate the same configuration on the secondary. can monitor the synchronization process on each geo node from the primary
node's Geo Nodes dashboard in your browser.
![GitLab Geo dashboard](img/geo-node-dashboard.png) ![GitLab Geo dashboard](img/geo-node-dashboard.png)
Disabling a secondary node stops the syncing process. If your installation isn't working properly, check the
[troubleshooting document](troubleshooting.md).
The two most obvious issues that replication can have here are: The two most obvious issues that can become apparent in the dashboard are:
1. Database replication not working well 1. Database replication not working well
1. Instance to instance notification not working. In that case, it can be 1. Instance to instance notification not working. In that case, it can be
...@@ -136,6 +129,13 @@ The two most obvious issues that replication can have here are: ...@@ -136,6 +129,13 @@ The two most obvious issues that replication can have here are:
[troubleshooting document](troubleshooting.md)) [troubleshooting document](troubleshooting.md))
- The instance is firewalled (check your firewall rules) - The instance is firewalled (check your firewall rules)
Please note that disabling a secondary node will stop the sync process.
Please note that if `git_data_dirs` is customized on the primary for multiple
repository shards you must duplicate the same configuration on the secondary.
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md).
Currently, this is what is synced: Currently, this is what is synced:
* Git repositories * Git repositories
......
...@@ -12,11 +12,9 @@ This is the final step in setting up a secondary Geo node. Stages of the setup ...@@ -12,11 +12,9 @@ This is the final step in setting up a secondary Geo node. Stages of the setup
process must be completed in the documented order. Before attempting the steps process must be completed in the documented order. Before attempting the steps
in this stage, [complete all prior stages](README.md#using-gitlab-installed-from-source). in this stage, [complete all prior stages](README.md#using-gitlab-installed-from-source).
The basic steps of configuring a secondary node are: The basic steps of configuring a secondary node are to replicate required
configurations between the primary and the secondaries; to configure a tracking
1. replicate required configurations between the primary and the secondaries; database on each secondary; and to start GitLab on the secondary node.
1. configure a second, tracking database on each secondary;
1. start GitLab on the secondary node's machine.
You are encouraged to first read through all the steps before executing them You are encouraged to first read through all the steps before executing them
in your testing/production environment. in your testing/production environment.
...@@ -94,42 +92,16 @@ cp primary.geo.example.com.crt /usr/local/share/ca-certificates ...@@ -94,42 +92,16 @@ cp primary.geo.example.com.crt /usr/local/share/ca-certificates
update-ca-certificates update-ca-certificates
``` ```
### Step 4. Managing the secondary GitLab node
Congratulations! Your secondary geo node is now configured!
The initial replication, or 'backfill', will probably still be in progress.
You can monitor the synchronization process on each geo node from the primary
node's Geo Nodes dashboard (Admin Area ➔ Geo Nodes, `/admin/geo_nodes`) in your
browser.
![GitLab Geo dashboard](img/geo-node-dashboard.png)
After the backfill is completed you can continue to monitor geo node health and
replication delays from the dashboard.
The two most obvious issues that can become apparent in the dashboard are:
1. Database replication not working well
1. Instance to instance notification not working. In that case, it can be
something of the following:
- You are using a custom certificate or custom CA (see the
### Step 4. Enable Git access over HTTP/HTTPS ### Step 4. Enable Git access over HTTP/HTTPS
- Instance is firewalled (check your firewall rules)
Please note that disabling a secondary node will stop the sync process.
Please note that if `git_data_dirs` is customized on the primary for multiple
repository shards you must duplicate the same configuration on the secondary.
GitLab Geo synchronizes repositories over HTTP/HTTPS, and so requires this clone GitLab Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
method to be enabled. Navigate to **Admin Area ➔ Settings** method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set (`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`. `Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Verify proper functioning of the secondary node ### Step 5. Verify proper functioning of the secondary node
Read [Verify proper functioning of the secondary node](configuration.md#verify-proper-functioning-of-the-secondary-node). Read [Verify proper functioning of the secondary node](configuration.md#step-5-verify-proper-functioning-of-the-secondary-node).
## Selective replication ## Selective replication
......
...@@ -112,8 +112,10 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -112,8 +112,10 @@ will not be able to perform all necessary configuration steps. Refer to
this example: this example:
```bash ```bash
# Certificate and key currently used by GitLab ##
# - replace primary.geo.example.com with your domain ## Certificate and key currently used by GitLab
## - replace primary.geo.example.com with your domain
##
install -o gitlab-psql -g gitlab-psql -m 0400 -T /etc/gitlab/ssl/primary.geo.example.com.crt ~gitlab-psql/data/server.crt install -o gitlab-psql -g gitlab-psql -m 0400 -T /etc/gitlab/ssl/primary.geo.example.com.crt ~gitlab-psql/data/server.crt
install -o gitlab-psql -g gitlab-psql -m 0400 -T /etc/gitlab/ssl/primary.geo.example.com.key ~gitlab-psql/data/server.key install -o gitlab-psql -g gitlab-psql -m 0400 -T /etc/gitlab/ssl/primary.geo.example.com.key ~gitlab-psql/data/server.key
``` ```
...@@ -134,8 +136,10 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -134,8 +136,10 @@ will not be able to perform all necessary configuration steps. Refer to
to the correct location: to the correct location:
``` ```
# Self-signed certificate and key ##
# - assumes the files are in your current working directory ## Self-signed certificate and key
## - assumes the files are in your current working directory
##
install -o gitlab-psql -g gitlab-psql -m 0400 -T server.crt ~gitlab-psql/data/server.crt install -o gitlab-psql -g gitlab-psql -m 0400 -T server.crt ~gitlab-psql/data/server.crt
install -o gitlab-psql -g gitlab-psql -m 0400 -T server.key ~gitlab-psql/data/server.key install -o gitlab-psql -g gitlab-psql -m 0400 -T server.key ~gitlab-psql/data/server.key
``` ```
...@@ -166,10 +170,14 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -166,10 +170,14 @@ will not be able to perform all necessary configuration steps. Refer to
To lookup the address of a Geo node, SSH in to the Geo node and execute: To lookup the address of a Geo node, SSH in to the Geo node and execute:
```bash ```bash
# Private address ##
## Private address
##
ip route get 255.255.255.255 | awk '{print "Private address:", $NF; exit}' ip route get 255.255.255.255 | awk '{print "Private address:", $NF; exit}'
# Public address ##
## Public address
##
echo "External address: $(curl ipinfo.io/ip)" echo "External address: $(curl ipinfo.io/ip)"
``` ```
...@@ -199,23 +207,31 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -199,23 +207,31 @@ will not be able to perform all necessary configuration steps. Refer to
```ruby ```ruby
geo_primary_role['enable'] = true geo_primary_role['enable'] = true
# Primary address ##
# - replace '1.2.3.4' with the primary private address ## Primary address
## - replace '1.2.3.4' with the primary private address
##
postgresql['listen_address'] = '1.2.3.4' postgresql['listen_address'] = '1.2.3.4'
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32'] postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32']
##
# Secondary addresses # Secondary addresses
# - replace '5.6.7.8' with the secondary public address # - replace '5.6.7.8' with the secondary public address
##
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32'] postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32']
# Replication settings ##
# - set this to be the number of Geo secondary nodes you have ## Replication settings
## - set this to be the number of Geo secondary nodes you have
##
postgresql['max_replication_slots'] = 1 postgresql['max_replication_slots'] = 1
# postgresql['max_wal_senders'] = 10 # postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10 # postgresql['wal_keep_segments'] = 10
# Disable automatic database migrations temporarily ##
# (until PostgreSQL is restarted and listening on the private address) ## Disable automatic database migrations temporarily
## (until PostgreSQL is restarted and listening on the private address).
##
gitlab_rails['auto_migrate'] = false gitlab_rails['auto_migrate'] = false
``` ```
...@@ -234,7 +250,7 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -234,7 +250,7 @@ will not be able to perform all necessary configuration steps. Refer to
1. Save the file and reconfigure GitLab for the database listen changes and 1. Save the file and reconfigure GitLab for the database listen changes and
the replication slot changes to be applied. the replication slot changes to be applied.
```bash ```bash
gitlab-ctl reconfigure gitlab-ctl reconfigure
``` ```
...@@ -322,13 +338,17 @@ because we have not yet configured the secondary server. This is the next step. ...@@ -322,13 +338,17 @@ because we have not yet configured the secondary server. This is the next step.
it in the right location. it in the right location.
```bash ```bash
# Certificate and key currently used by GitLab ##
## Certificate and key currently used by GitLab
##
mkdir -p ~gitlab-psql/.postgresql mkdir -p ~gitlab-psql/.postgresql
ln -s /opt/gitlab/embedded/ssl/certs/cacert.pem ~gitlab-psql/.postgresql/root.crt ln -s /opt/gitlab/embedded/ssl/certs/cacert.pem ~gitlab-psql/.postgresql/root.crt
``` ```
```bash ```bash
# Self-signed certificate and key ##
## Self-signed certificate and key
##
install -o gitlab-psql -g gitlab-psql -m 0400 -T server.crt -D ~gitlab-psql/.postgresql/root.crt install -o gitlab-psql -g gitlab-psql -m 0400 -T server.crt -D ~gitlab-psql/.postgresql/root.crt
``` ```
...@@ -336,15 +356,20 @@ because we have not yet configured the secondary server. This is the next step. ...@@ -336,15 +356,20 @@ because we have not yet configured the secondary server. This is the next step.
connections. connections.
1. Test that the remote connection to the primary server works. 1. Test that the remote connection to the primary server works (as the
`gitlab-psql` user):
``` ```
# Certificate and key currently used by GitLab, and connecting by FQDN ##
## Certificate and key currently used by GitLab, and connecting by FQDN
##
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-full" -W -h primary.geo.example.com sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-full" -W -h primary.geo.example.com
``` ```
``` ```
# Self-signed certificate and key, or connecting by IP address ##
## Self-signed certificate and key, or connecting by IP address
##
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-ca" -W -h 1.2.3.4 sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-ca" -W -h 1.2.3.4
``` ```
...@@ -417,13 +442,17 @@ data before running `pg_basebackup`. ...@@ -417,13 +442,17 @@ data before running `pg_basebackup`.
1. Execute the command below to start a backup/restore and begin the replication: 1. Execute the command below to start a backup/restore and begin the replication:
``` ```
# Certificate and key currently used by GitLab, and connecting by FQDN ##
## Certificate and key currently used by GitLab, and connecting by FQDN
##
gitlab-ctl replicate-geo-database --slot-name=secondary_example --host=primary.geo.example.com gitlab-ctl replicate-geo-database --slot-name=secondary_example --host=primary.geo.example.com
``` ```
``` ```
# Self-signed certificate and key, or connecting by IP ##
gitlab-ctl replicate-geo-database --slot-name=secondary_example --sslmode=verify-ca --host=1.2.3.4 ## Self-signed certificate and key, or connecting by IP
##
gitlab-ctl replicate-geo-database --sslmode=verify-ca --slot-name=secondary_example --host=1.2.3.4
``` ```
If PostgreSQL is listening on a non-standard port, add `--port=` as well. If PostgreSQL is listening on a non-standard port, add `--port=` as well.
...@@ -477,14 +506,18 @@ The `geo_primary_role` makes configuration changes to `pg_hba.conf` and ...@@ -477,14 +506,18 @@ The `geo_primary_role` makes configuration changes to `pg_hba.conf` and
`postgresql.conf` on the primary: `postgresql.conf` on the primary:
``` ```
# pg_hba.conf ##
# GitLab Geo Primary ## GitLab Geo Primary
## - pg_hba.conf
##
host replication gitlab_replicator <trusted secondary IP>/32 md5 host replication gitlab_replicator <trusted secondary IP>/32 md5
``` ```
``` ```
# postgresql.conf ##
# Geo Primary Role ## Geo Primary Role
## - postgresql.conf
##
sql_replication_user = gitlab_replicator sql_replication_user = gitlab_replicator
wal_level = hot_standby wal_level = hot_standby
max_wal_senders = 10 max_wal_senders = 10
...@@ -499,8 +532,10 @@ on the secondary. The PostgreSQL settings for this database it adds to ...@@ -499,8 +532,10 @@ on the secondary. The PostgreSQL settings for this database it adds to
the default settings: the default settings:
``` ```
# postgresql.conf ##
# Geo Secondary Role ## Geo Secondary Role
## - postgresql.conf
##
wal_level = hot_standby wal_level = hot_standby
max_wal_senders = 10 max_wal_senders = 10
wal_keep_segments = 10 wal_keep_segments = 10
......
# GitLab Geo High Availability
This document describes a possible configuration on how to set up Geo
in a Highly Available environment. If your HA setup differs from the one
described in this document, you still can use the instructions and adapt them
to your needs.
## Architecture overview
![Active/Active HA Diagram](../administration/img/high_availability/active-active-diagram.png)
This documentation assumes all machines used in this HA setup can
communicate over the network using internal IP addresses.
NOTE: **Note:**
`external_url` must be the same for every machine, and `https` should be used.
## Services machine
One machine, called the Services machine will be used to run:
- NFS shares
- PostgreSQL
- Redis
- HAProxy
### Prerequisites
Make sure you have GitLab EE installed using the
[Omnibus package](https://about.gitlab.com/installation).
The following steps should be performed in the Services machine. SSH to it
and login as root:
```sh
sudo -i
```
### Step 1: Set up NFS share
1. Install the required NFS packages:
```sh
apt-get install nfs-kernel-server
```
1. Create the required directories:
```sh
mkdir -p /var/opt/gitlab/nfs/builds/ \
/var/opt/gitlab/nfs/git-data/ \
/var/opt/gitlab/nfs/shared/ \
/var/opt/gitlab/nfs/uploads/
```
1. Make the directories available through NFS, by adding this to
`/etc/exports` (see also the [NFS HA recommended options](../administration/high_availability/nfs.md#recommended-options)):
```
/var/opt/gitlab/nfs *(rw,sync,no_root_squash)
```
1. Start the NFS service:
```sh
systemctl start nfs-kernel-server.service
```
1. Apply the settings to take effect:
```sh
exportfs -a
```
### Step 2: Set up PostgreSQL server
1. Edit `/etc/gitlab/gitlab.rb` and add the following:
```ruby
postgresql['enable'] = true
##
## Replace 1.2.3.4 with the internal IP address of the current machine and
## 2.3.4.5 and 3.4.5.6 with the internal IP addresses of the machines
## running the Application server(s).
##
postgresql['listen_address'] = '1.2.3.4'
postgresql['trust_auth_cidr_addresses'] = ['1.2.3.4/32', '2.3.4.5/32', '3.4.5.6/32']
gitlab_rails['auto_migrate'] = true
gitlab_rails['db_password'] = 'DB password'
```
1. **Only for secondary nodes** Also add this to `/etc/gitlab/gitlab.rb`:
```ruby
geo_postgresql['enable'] = true
##
## Replace 1.2.3.4 with the internal IP address of the current machine and
## 2.3.4.5 and 3.4.5.6 with the internal IP addresses of the machines
## running the Application server(s).
##
geo_postgresql['listen_address'] = '1.2.3.4'
geo_postgresql['trust_auth_cidr_addresses'] = ['1.2.3.4/32', '2.3.4.5/32', '3.4.5.6/32']
geo_secondary['auto_migrate'] = true
geo_secondary['db_host'] = '1.2.3.4'
geo_secondary['db_password'] = 'Geo tracking DB password'
```
### Step 3: Set up Redis
Edit `/etc/gitlab/gitlab.rb` and add the following:
```ruby
redis['enable'] = true
redis['password'] = 'Redis password'
##
## Replace 1.2.3.4 with the internal IP address of the current machine
##
redis['bind'] = '1.2.3.4'
redis['port'] = 6379
##
## Needed because 'gitlab-ctl reconfigure' runs 'rake cache:clear:redis'
##
gitlab_rails['redis_password'] = 'Redis password'
```
### Step 4: HAProxy
We'll be using HAProxy to balance the load between the Application machines.
1. Manually stop Nginx (we will disable it in `/etc/gitlab/gitlab.rb` later):
```sh
gitlab-ctl stop nginx
```
1. Install the HAProxy package:
```sh
apt-get install haproxy
```
1. Make sure you have a single SSL `pem` file containing the
certificate and the private key.
```sh
cat /etc/ssl/cert.pem /etc/ssl/privkey.pem > /etc/ssl/aio.pem
```
1. Edit `/etc/haproxy/haproxy.cfg` and overwrite it with the following:
```
global
log 127.0.0.1 local0
log 127.0.0.1 local1 notice
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
stats enable
stats uri /haproxy?stats
frontend www-http
bind *:80
reqadd X-Forwarded-Proto:\ http
default_backend www-backend
frontend www-https
bind 0.0.0.0:443 ssl crt /etc/ssl/aio.pem
reqadd X-Forwarded-Proto:\ https
default_backend www-backend
backend www-backend
redirect scheme https if !{ ssl_fc }
balance leastconn
option httpclose
option forwardfor
cookie JSESSIONID prefix
##
## Enter the IPs of your Application servers here
##
server nodeA 2.3.4.5:80 cookie A check
server nodeB 3.4.5.6:80 cookie A check
```
1. Start the HAProxy service:
```sh
service haproxy start
```
### Step 5: Apply settings
1. Edit `/etc/gitlab/gitlab.rb` and add the following:
```ruby
nginx['enable'] = false
sidekiq['enable'] = false
unicorn['enable'] = false
##
## These are optional/untested/irrelevant
##
gitaly['enable'] = false
gitlab_workhorse['enable'] = false
mailroom['enable'] = false
prometheus['enable'] = false
```
1. [Reconfigure GitLab][] for the changes to take effect.
1. Until [Omnibus#2797](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2797)
gets fixed, you will need to manually restart PostgreSQL:
```sh
gitlab-ctl restart postgresql geo-postgresql
```
### Step 6: Step up database replication
Database replication will operate between the Services machines.
Follow the [Setup the database replication](database.md) instructions
to set up.
## Application machine
Repeat these steps for every machine running `gitlab-rails`.
The following steps should be performed in the Application machine. SSH to it
and login as root:
```sh
sudo -i
```
### Step 1: Add NFS mount
1. Install the required NFS packages:
```sh
apt-get install nfs-common
```
1. Create the mount point directory:
```sh
mkdir -p /mnt/nfs
```
1. Edit `/etc/fstab` and add the following lines
(where `1.2.3.4` is the internal IP of the Services machine):
```
1.2.3.4:/var/opt/gitlab/nfs /mnt/nfs nfs defaults,nfsvers=4,soft,rsize=1048576,wsize=1048576,noatime
```
1. Mount the share:
```sh
mount -a -t nfs
```
You can check if the mount is working by checking the existence of the
directories `builds/`, `git-data/`, `shared/`, and `uploads/` in
`/mnt/nfs`:
```
ls /mnt/nfs
```
### Step 2: Configure proxied SSL
The load balancer will take care of the SSL termination, so configure nginx to
work with proxied SSL.
Follow the instructions to [configure proxied SSL](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl).
### Step 3: Configure connections to the Services machine
1. Edit `/etc/gitlab/gitlab.rb` and add the following:
```ruby
##
## Use the NFS mount to store data
##
gitlab_rails['uploads_directory'] = '/mnt/nfs/uploads'
gitlab_rails['shared_path'] = '/mnt/nfs/shared'
gitlab_ci['builds_directory'] = '/mnt/nfs/builds'
git_data_dirs({
'default': {
'path': '/mnt/nfs/git-data'
}
})
high_availability['mountpoint'] = '/mnt/nfs'
##
## Disable PostgreSQL on the local machine and connect to the remote
##
postgresql['enable'] = false
gitlab_rails['auto_migrate'] = false
gitlab_rails['db_host'] = '1.2.3.4'
gitlab_rails['db_password'] = 'DB password'
##
## Disable Redis on the local machine and connect to the remote
##
redis['enable'] = false
gitlab_rails['redis_host'] = '1.2.3.4'
gitlab_rails['redis_password'] = 'Redis password'
```
1. **[Only for primary nodes]** Add the following to `/etc/gitlab/gitlab.rb`:
```ruby
geo_primary_role['enable'] = true
```
1. **[Only for secondary nodes]** Add the following to `/etc/gitlab/gitlab.rb`:
```ruby
geo_secondary_role['enable'] = true
geo_postgresql['enable'] = true # TODO set to false when https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2980 is fixed
geo_secondary['auto_migrate'] = false
geo_secondary['db_host'] = '1.2.3.4'
geo_secondary['db_password'] = 'Geo tracking DB password'
```
1. Copy the database encryption key. Follow the instructions of
[Step 1. Copying the database encryption key](configuration.md#step-1-copying-the-database-encryption-key)
1. [Reconfigure GitLab][] for the changes to take effect (if you haven't done
this yet in previous step).
1. [Restart GitLab][] to start the processes with the correct connections.
## Troubleshooting
### HAProxy
You can connect to `https://example.com/haproxy?stats` to monitor the
load balancing between the Application machines.
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-restart
...@@ -88,7 +88,7 @@ the default thirty minutes. Adjust as required for your installation. ...@@ -88,7 +88,7 @@ the default thirty minutes. Adjust as required for your installation.
Determine if you have any unused replication slots in the primary database. This can cause large amounts of log data to build up in `pg_xlog`. Determine if you have any unused replication slots in the primary database. This can cause large amounts of log data to build up in `pg_xlog`.
Removing the unused slots can reduce the amount of space used in the `pg_xlog`. Removing the unused slots can reduce the amount of space used in the `pg_xlog`.
- Start a PostgreSQL console session: 1. Start a PostgreSQL console session:
```bash ```bash
sudo gitlab-psql gitlabhq_production sudo gitlab-psql gitlabhq_production
...@@ -96,17 +96,22 @@ Removing the unused slots can reduce the amount of space used in the `pg_xlog`. ...@@ -96,17 +96,22 @@ Removing the unused slots can reduce the amount of space used in the `pg_xlog`.
Note that using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions. Note that using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
- View your replication slots with 2. View your replication slots with
```sql ```sql
SELECT * FROM pg_replication_slots; SELECT * FROM pg_replication_slots;
``` ```
- If there is an unused/inactive slot, you can remove it with Slots where `active` is `f` are not active.
```sql - When this slot should be active, because you have a secondary configured using that slot,
SELECT pg_drop_replication_slot('name_of_extra_slot'); log in to that secondary and check the PostgreSQL logs why the replication is not running.
```
- If you are no longer using the slot (e.g. you no longer have Geo enabled), you can remove it with in the PostgreSQL console session:
```sql
SELECT pg_drop_replication_slot('name_of_extra_slot');
```
#### Very large repositories never successfully synchronize on the secondary #### Very large repositories never successfully synchronize on the secondary
......
...@@ -4,6 +4,7 @@ module API ...@@ -4,6 +4,7 @@ module API
before { authenticate_by_gitlab_shell_token! } before { authenticate_by_gitlab_shell_token! }
helpers ::API::Helpers::InternalHelpers helpers ::API::Helpers::InternalHelpers
helpers ::Gitlab::Identifier
namespace 'internal' do namespace 'internal' do
# Check if git command is allowed to project # Check if git command is allowed to project
...@@ -188,17 +189,25 @@ module API ...@@ -188,17 +189,25 @@ module API
post '/post_receive' do post '/post_receive' do
status 200 status 200
PostReceive.perform_async(params[:gl_repository], params[:identifier], PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes]) params[:changes])
broadcast_message = BroadcastMessage.current&.last&.message broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
{ output = {
merge_request_urls: merge_request_urls, merge_request_urls: merge_request_urls,
broadcast_message: broadcast_message, broadcast_message: broadcast_message,
reference_counter_decreased: reference_counter_decreased reference_counter_decreased: reference_counter_decreased
} }
project = Gitlab::GlRepository.parse(params[:gl_repository]).first
user = identify(params[:identifier])
redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
if redirect_message
output[:redirected_message] = redirect_message
end
output
end end
end end
end end
......
module Gitlab
module Checks
class ProjectMoved
REDIRECT_NAMESPACE = "redirect_namespace".freeze
def initialize(project, user, redirected_path, protocol)
@project = project
@user = user
@redirected_path = redirected_path
@protocol = protocol
end
def self.fetch_redirect_message(user_id, project_id)
redirect_key = redirect_message_key(user_id, project_id)
Gitlab::Redis::SharedState.with do |redis|
message = redis.get(redirect_key)
redis.del(redirect_key)
message
end
end
def add_redirect_message
Gitlab::Redis::SharedState.with do |redis|
key = self.class.redirect_message_key(user.id, project.id)
redis.setex(key, 5.minutes, redirect_message)
end
end
def redirect_message(rejected: false)
<<~MESSAGE.strip_heredoc
Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote:
#{remote_url_message(rejected)}
MESSAGE
end
def permanent_redirect?
RedirectRoute.permanent.exists?(path: redirected_path)
end
private
attr_reader :project, :redirected_path, :protocol, :user
def self.redirect_message_key(user_id, project_id)
"#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
end
def remote_url_message(rejected)
if rejected
"git remote set-url origin #{url} and try again."
else
"git remote set-url origin #{url}"
end
end
def url
protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
end
end
end
end
...@@ -3,14 +3,13 @@ module Gitlab ...@@ -3,14 +3,13 @@ module Gitlab
module Pipeline module Pipeline
module Chain module Chain
class Base class Base
attr_reader :pipeline, :project, :current_user attr_reader :pipeline, :command
delegate :project, :current_user, to: :command
def initialize(pipeline, command) def initialize(pipeline, command)
@pipeline = pipeline @pipeline = pipeline
@command = command @command = command
@project = command.project
@current_user = command.current_user
end end
def perform! def perform!
......
...@@ -3,20 +3,18 @@ module Gitlab ...@@ -3,20 +3,18 @@ module Gitlab
module Pipeline module Pipeline
module Chain module Chain
class Build < Chain::Base class Build < Chain::Base
include Chain::Helpers
def perform! def perform!
@pipeline.assign_attributes( @pipeline.assign_attributes(
source: @command.source, source: @command.source,
project: @project, project: @command.project,
ref: ref, ref: @command.ref,
sha: sha, sha: @command.sha,
before_sha: before_sha, before_sha: @command.before_sha,
tag: tag_exists?, tag: @command.tag_exists?,
trigger_requests: Array(@command.trigger_request), trigger_requests: Array(@command.trigger_request),
user: @current_user, user: @command.current_user,
pipeline_schedule: @command.schedule, pipeline_schedule: @command.schedule,
protected: protected_ref? protected: @command.protected_ref?
) )
@pipeline.set_config_source @pipeline.set_config_source
...@@ -25,32 +23,6 @@ module Gitlab ...@@ -25,32 +23,6 @@ module Gitlab
def break? def break?
false false
end end
private
def ref
@ref ||= Gitlab::Git.ref_name(origin_ref)
end
def sha
@project.commit(origin_sha || origin_ref).try(:id)
end
def origin_ref
@command.origin_ref
end
def origin_sha
@command.checkout_sha || @command.after_sha
end
def before_sha
@command.checkout_sha || @command.before_sha || Gitlab::Git::BLANK_SHA
end
def protected_ref?
@project.protected_for?(ref)
end
end end
end end
end end
......
module Gitlab
module Ci
module Pipeline
module Chain
Command = Struct.new(
:source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha,
:trigger_request, :schedule,
:ignore_skip_ci, :save_incompleted,
:seeds_block,
# EE specific
:allow_mirror_update
) do
include Gitlab::Utils::StrongMemoize
def initialize(**params)
params.each do |key, value|
self[key] = value
end
end
def branch_exists?
strong_memoize(:is_branch) do
project.repository.branch_exists?(ref)
end
end
def tag_exists?
strong_memoize(:is_tag) do
project.repository.tag_exists?(ref)
end
end
def ref
strong_memoize(:ref) do
Gitlab::Git.ref_name(origin_ref)
end
end
def sha
strong_memoize(:sha) do
project.commit(origin_sha || origin_ref).try(:id)
end
end
def origin_sha
checkout_sha || after_sha
end
def before_sha
self[:before_sha] || checkout_sha || Gitlab::Git::BLANK_SHA
end
def protected_ref?
strong_memoize(:protected_ref) do
project.protected_for?(ref)
end
end
end
end
end
end
end
...@@ -3,18 +3,6 @@ module Gitlab ...@@ -3,18 +3,6 @@ module Gitlab
module Pipeline module Pipeline
module Chain module Chain
module Helpers module Helpers
def branch_exists?
return @is_branch if defined?(@is_branch)
@is_branch = project.repository.branch_exists?(pipeline.ref)
end
def tag_exists?
return @is_tag if defined?(@is_tag)
@is_tag = project.repository.tag_exists?(pipeline.ref)
end
def error(message) def error(message)
pipeline.errors.add(:base, message) pipeline.errors.add(:base, message)
end end
......
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
unless allowed_to_trigger_pipeline? unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project) if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{pipeline.ref}'") return error("Insufficient permissions for protected ref '#{command.ref}'")
else else
return error('Insufficient permissions to create a new pipeline') return error('Insufficient permissions to create a new pipeline')
end end
...@@ -33,7 +33,7 @@ module Gitlab ...@@ -33,7 +33,7 @@ module Gitlab
if current_user if current_user
allowed_to_create? allowed_to_create?
else # legacy triggers don't have a corresponding user else # legacy triggers don't have a corresponding user
!project.protected_for?(@pipeline.ref) !@command.protected_ref?
end end
end end
...@@ -42,10 +42,10 @@ module Gitlab ...@@ -42,10 +42,10 @@ module Gitlab
access = Gitlab::UserAccess.new(current_user, project: project) access = Gitlab::UserAccess.new(current_user, project: project)
if branch_exists? if @command.branch_exists?
access.can_update_branch?(@pipeline.ref) access.can_update_branch?(@command.ref)
elsif tag_exists? elsif @command.tag_exists?
access.can_create_tag?(@pipeline.ref) access.can_create_tag?(@command.ref)
else else
true # Allow it for now and we'll reject when we check ref existence true # Allow it for now and we'll reject when we check ref existence
end end
......
...@@ -7,14 +7,11 @@ module Gitlab ...@@ -7,14 +7,11 @@ module Gitlab
include Chain::Helpers include Chain::Helpers
def perform! def perform!
unless branch_exists? || tag_exists? unless @command.branch_exists? || @command.tag_exists?
return error('Reference not found') return error('Reference not found')
end end
## TODO, we check commit in the service, that is why unless @command.sha
# there is no repository access here.
#
unless pipeline.sha
return error('Commit not found') return error('Commit not found')
end end
end end
......
...@@ -106,18 +106,15 @@ module Gitlab ...@@ -106,18 +106,15 @@ module Gitlab
end end
def check_project_moved! def check_project_moved!
return unless redirected_path return if redirected_path.nil?
url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol)
message = <<-MESSAGE.strip_heredoc
Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote and try again: if project_moved.permanent_redirect?
project_moved.add_redirect_message
git remote set-url origin #{url} else
MESSAGE raise ProjectMovedError, project_moved.redirect_message(rejected: true)
end
raise ProjectMovedError, message
end end
def check_command_disabled!(cmd) def check_command_disabled!(cmd)
......
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
# key-13 or user-36 or last commit # key-13 or user-36 or last commit
module Gitlab module Gitlab
module Identifier module Identifier
def identify(identifier, project, newrev) def identify(identifier, project = nil, newrev = nil)
if identifier.blank? if identifier.blank?
# Local push from gitlab
identify_using_commit(project, newrev) identify_using_commit(project, newrev)
elsif identifier =~ /\Auser-\d+\Z/ elsif identifier =~ /\Auser-\d+\Z/
# git push over http # git push over http
...@@ -17,6 +16,8 @@ module Gitlab ...@@ -17,6 +16,8 @@ module Gitlab
# Tries to identify a user based on a commit SHA. # Tries to identify a user based on a commit SHA.
def identify_using_commit(project, ref) def identify_using_commit(project, ref)
return if project.nil? && ref.nil?
commit = project.commit(ref) commit = project.commit(ref)
return if !commit || !commit.author_email return if !commit || !commit.author_email
......
...@@ -272,6 +272,20 @@ describe Projects::IssuesController do ...@@ -272,6 +272,20 @@ describe Projects::IssuesController do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(issue.reload.title).to eq('New title') expect(issue.reload.title).to eq('New title')
end end
context 'when Akismet is enabled and the issue is identified as spam' do
before do
stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
it 'renders json with recaptcha_html' do
subject
expect(JSON.parse(response.body)).to have_key('recaptcha_html')
end
end
end end
context 'when user does not have access to update issue' do context 'when user does not have access to update issue' do
...@@ -504,17 +518,16 @@ describe Projects::IssuesController do ...@@ -504,17 +518,16 @@ describe Projects::IssuesController do
expect(spam_logs.first.recaptcha_verified).to be_falsey expect(spam_logs.first.recaptcha_verified).to be_falsey
end end
it 'renders json errors' do it 'renders recaptcha_html json response' do
update_issue update_issue
expect(json_response) expect(json_response).to have_key('recaptcha_html')
.to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
end end
it 'returns 422 status' do it 'returns 200 status' do
update_issue update_issue
expect(response).to have_gitlab_http_status(422) expect(response).to have_gitlab_http_status(200)
end end
end end
......
...@@ -5,6 +5,7 @@ import * as urlUtils from '~/lib/utils/url_utility'; ...@@ -5,6 +5,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub'; import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data'; import issueShowData from '../mock_data';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) { function formatText(text) {
return text.trim().replace(/\s\s+/g, ' '); return text.trim().replace(/\s\s+/g, ' ');
...@@ -56,6 +57,8 @@ describe('Issuable output', () => { ...@@ -56,6 +57,8 @@ describe('Issuable output', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm.poll.stop(); vm.poll.stop();
vm.$destroy();
}); });
it('should render a title/description/edited and update title/description/edited on update', (done) => { it('should render a title/description/edited and update title/description/edited on update', (done) => {
...@@ -269,6 +272,52 @@ describe('Issuable output', () => { ...@@ -269,6 +272,52 @@ describe('Issuable output', () => {
}); });
}); });
it('opens recaptcha dialog if update rejected as spam', (done) => {
function mockScriptSrc() {
const recaptchaChild = vm.$children
.find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle
recaptchaChild.scriptSrc = '//scriptsrc';
}
let modal;
const promise = new Promise((resolve) => {
resolve({
json() {
return {
recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
};
},
});
});
spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
vm.canUpdate = true;
vm.showForm = true;
vm.$nextTick()
.then(() => mockScriptSrc())
.then(() => vm.updateIssuable())
.then(promise)
.then(() => setTimeoutPromise())
.then(() => {
modal = vm.$el.querySelector('.js-recaptcha-dialog');
expect(modal.style.display).not.toEqual('none');
expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
})
.then(() => modal.querySelector('.close').click())
.then(() => vm.$nextTick())
.then(() => {
expect(modal.style.display).toEqual('none');
expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
})
.then(done)
.catch(done.fail);
});
describe('deleteIssuable', () => { describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => { it('changes URL when deleted', (done) => {
spyOn(urlUtils, 'visitUrl'); spyOn(urlUtils, 'visitUrl');
......
...@@ -51,6 +51,35 @@ describe('Description component', () => { ...@@ -51,6 +51,35 @@ describe('Description component', () => {
}); });
}); });
it('opens recaptcha dialog if update rejected as spam', (done) => {
let modal;
const recaptchaChild = vm.$children
.find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle
recaptchaChild.scriptSrc = '//scriptsrc';
vm.taskListUpdateSuccess({
recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
});
vm.$nextTick()
.then(() => {
modal = vm.$el.querySelector('.js-recaptcha-dialog');
expect(modal.style.display).not.toEqual('none');
expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
})
.then(() => modal.querySelector('.close').click())
.then(() => vm.$nextTick())
.then(() => {
expect(modal.style.display).toEqual('none');
expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
})
.then(done)
.catch(done.fail);
});
describe('TaskList', () => { describe('TaskList', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, { vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
...@@ -86,6 +115,7 @@ describe('Description component', () => { ...@@ -86,6 +115,7 @@ describe('Description component', () => {
dataType: 'issuableType', dataType: 'issuableType',
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
onSuccess: jasmine.any(Function),
}); });
done(); done();
}); });
......
import Vue from 'vue';
import PopupDialog from '~/vue_shared/components/popup_dialog.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('PopupDialog', () => {
it('does not render a primary button if no primaryButtonLabel', () => {
const popupDialog = Vue.extend(PopupDialog);
const vm = mountComponent(popupDialog);
expect(vm.$el.querySelector('.js-primary-button')).toBeNull();
});
});
require 'rails_helper'
describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:project) { create(:project) }
describe '.fetch_redirct_message' do
context 'with a redirect message queue' do
it 'should return the redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http')
project_moved.add_redirect_message
expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message)
end
it 'should delete the redirect message from redis' do
project_moved = described_class.new(project, user, 'foo/bar', 'http')
project_moved.add_redirect_message
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil
described_class.fetch_redirect_message(user.id, project.id)
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
end
end
context 'with no redirect message queue' do
it 'should return nil' do
expect(described_class.fetch_redirect_message(1, 2)).to be_nil
end
end
end
describe '#add_redirect_message' do
it 'should queue a redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http')
expect(project_moved.add_redirect_message).to eq("OK")
end
end
describe '#redirect_message' do
context 'when the push is rejected' do
it 'should return a redirect message telling the user to try again' do
project_moved = described_class.new(project, user, 'foo/bar', 'http')
message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
"\n\nPlease update your Git remote:" +
"\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
expect(project_moved.redirect_message(rejected: true)).to eq(message)
end
end
context 'when the push is not rejected' do
it 'should return a redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http')
message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
"\n\nPlease update your Git remote:" +
"\n\n git remote set-url origin #{project.http_url_to_repo}\n"
expect(project_moved.redirect_message).to eq(message)
end
end
end
describe '#permanent_redirect?' do
context 'with a permanent RedirectRoute' do
it 'should return true' do
project.route.create_redirect('foo/bar', permanent: true)
project_moved = described_class.new(project, user, 'foo/bar', 'http')
expect(project_moved.permanent_redirect?).to be_truthy
end
end
context 'without a permanent RedirectRoute' do
it 'should return false' do
project.route.create_redirect('foo/bar')
project_moved = described_class.new(project, user, 'foo/bar', 'http')
expect(project_moved.permanent_redirect?).to be_falsy
end
end
end
end
...@@ -6,46 +6,81 @@ describe Gitlab::Ci::Pipeline::Chain::Build do ...@@ -6,46 +6,81 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
let(:pipeline) { Ci::Pipeline.new } let(:pipeline) { Ci::Pipeline.new }
let(:command) do let(:command) do
double('command', source: :push, Gitlab::Ci::Pipeline::Chain::Command.new(
origin_ref: 'master', source: :push,
checkout_sha: project.commit.id, origin_ref: 'master',
after_sha: nil, checkout_sha: project.commit.id,
before_sha: nil, after_sha: nil,
trigger_request: nil, before_sha: nil,
schedule: nil, trigger_request: nil,
project: project, schedule: nil,
current_user: user) project: project,
current_user: user)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
before do before do
stub_repository_ci_yaml_file(sha: anything) stub_repository_ci_yaml_file(sha: anything)
step.perform!
end end
it 'never breaks the chain' do it 'never breaks the chain' do
step.perform!
expect(step.break?).to be false expect(step.break?).to be false
end end
it 'fills pipeline object with data' do it 'fills pipeline object with data' do
step.perform!
expect(pipeline.sha).not_to be_empty expect(pipeline.sha).not_to be_empty
expect(pipeline.sha).to eq project.commit.id expect(pipeline.sha).to eq project.commit.id
expect(pipeline.ref).to eq 'master' expect(pipeline.ref).to eq 'master'
expect(pipeline.tag).to be false
expect(pipeline.user).to eq user expect(pipeline.user).to eq user
expect(pipeline.project).to eq project expect(pipeline.project).to eq project
end end
it 'sets a valid config source' do it 'sets a valid config source' do
step.perform!
expect(pipeline.repository_source?).to be true expect(pipeline.repository_source?).to be true
end end
it 'returns a valid pipeline' do it 'returns a valid pipeline' do
step.perform!
expect(pipeline).to be_valid expect(pipeline).to be_valid
end end
it 'does not persist a pipeline' do it 'does not persist a pipeline' do
step.perform!
expect(pipeline).not_to be_persisted expect(pipeline).not_to be_persisted
end end
context 'when pipeline is running for a tag' do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
origin_ref: 'mytag',
checkout_sha: project.commit.id,
after_sha: nil,
before_sha: nil,
trigger_request: nil,
schedule: nil,
project: project,
current_user: user)
end
before do
allow_any_instance_of(Repository).to receive(:tag_exists?).with('mytag').and_return(true)
step.perform!
end
it 'correctly indicated that this is a tagged pipeline' do
expect(pipeline).to be_tag
end
end
end end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Command do
set(:project) { create(:project, :repository) }
describe '#initialize' do
subject do
described_class.new(origin_ref: 'master')
end
it 'properly initialises object from hash' do
expect(subject.origin_ref).to eq('master')
end
end
context 'handling of origin_ref' do
let(:command) { described_class.new(project: project, origin_ref: origin_ref) }
describe '#branch_exists?' do
subject { command.branch_exists? }
context 'for existing branch' do
let(:origin_ref) { 'master' }
it { is_expected.to eq(true) }
end
context 'for invalid branch' do
let(:origin_ref) { 'something' }
it { is_expected.to eq(false) }
end
end
describe '#tag_exists?' do
subject { command.tag_exists? }
context 'for existing ref' do
let(:origin_ref) { 'v1.0.0' }
it { is_expected.to eq(true) }
end
context 'for invalid ref' do
let(:origin_ref) { 'something' }
it { is_expected.to eq(false) }
end
end
describe '#ref' do
subject { command.ref }
context 'for regular ref' do
let(:origin_ref) { 'master' }
it { is_expected.to eq('master') }
end
context 'for branch ref' do
let(:origin_ref) { 'refs/heads/master' }
it { is_expected.to eq('master') }
end
context 'for tag ref' do
let(:origin_ref) { 'refs/tags/1.0.0' }
it { is_expected.to eq('1.0.0') }
end
context 'for other refs' do
let(:origin_ref) { 'refs/merge-requests/11/head' }
it { is_expected.to eq('refs/merge-requests/11/head') }
end
end
end
describe '#sha' do
subject { command.sha }
context 'when invalid checkout_sha is specified' do
let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
it 'returns empty value' do
is_expected.to be_nil
end
end
context 'when a valid checkout_sha is specified' do
let(:command) { described_class.new(project: project, checkout_sha: project.commit.id) }
it 'returns checkout_sha' do
is_expected.to eq(project.commit.id)
end
end
context 'when a valid after_sha is specified' do
let(:command) { described_class.new(project: project, after_sha: project.commit.id) }
it 'returns after_sha' do
is_expected.to eq(project.commit.id)
end
end
context 'when a valid origin_ref is specified' do
let(:command) { described_class.new(project: project, origin_ref: 'HEAD') }
it 'returns SHA for given ref' do
is_expected.to eq(project.commit.id)
end
end
end
describe '#origin_sha' do
subject { command.origin_sha }
context 'when using checkout_sha and after_sha' do
let(:command) { described_class.new(project: project, checkout_sha: 'aaa', after_sha: 'bbb') }
it 'uses checkout_sha' do
is_expected.to eq('aaa')
end
end
context 'when using after_sha only' do
let(:command) { described_class.new(project: project, after_sha: 'bbb') }
it 'uses after_sha' do
is_expected.to eq('bbb')
end
end
end
describe '#before_sha' do
subject { command.before_sha }
context 'when using checkout_sha and before_sha' do
let(:command) { described_class.new(project: project, checkout_sha: 'aaa', before_sha: 'bbb') }
it 'uses before_sha' do
is_expected.to eq('bbb')
end
end
context 'when using checkout_sha only' do
let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
it 'uses checkout_sha' do
is_expected.to eq('aaa')
end
end
context 'when checkout_sha and before_sha are empty' do
let(:command) { described_class.new(project: project) }
it 'uses BLANK_SHA' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
end
end
end
describe '#protected_ref?' do
let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
subject { command.protected_ref? }
context 'when a ref is protected' do
before do
expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(true)
end
it { is_expected.to eq(true) }
end
context 'when a ref is unprotected' do
before do
expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(false)
end
it { is_expected.to eq(false) }
end
end
end
...@@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
end end
let(:command) do let(:command) do
double('command', project: project, Gitlab::Ci::Pipeline::Chain::Command.new(
current_user: user, project: project,
seeds_block: nil) current_user: user, seeds_block: nil)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
......
...@@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do ...@@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) } let(:pipeline) { build_stubbed(:ci_pipeline) }
let(:command) { double('command' ) } let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') } let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') } let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] } let(:sequence) { [first_step, second_step] }
......
...@@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do ...@@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
set(:pipeline) { create(:ci_pipeline, project: project) } set(:pipeline) { create(:ci_pipeline, project: project) }
let(:command) do let(:command) do
double('command', project: project, Gitlab::Ci::Pipeline::Chain::Command.new(
current_user: user, project: project,
ignore_skip_ci: false, current_user: user,
save_incompleted: true) ignore_skip_ci: false,
save_incompleted: true)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
......
...@@ -5,13 +5,13 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do ...@@ -5,13 +5,13 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:pipeline) do let(:pipeline) do
build_stubbed(:ci_pipeline, ref: ref, project: project) build_stubbed(:ci_pipeline, project: project)
end end
let(:command) do let(:command) do
double('command', project: project, Gitlab::Ci::Pipeline::Chain::Command.new(
current_user: user, project: project, current_user: user, origin_ref: ref,
allow_mirror_update: false) allow_mirror_update: false)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
......
...@@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do ...@@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:command) do let(:command) do
double('command', project: project, Gitlab::Ci::Pipeline::Chain::Command.new(
current_user: user, project: project,
save_incompleted: true) current_user: user,
save_incompleted: true)
end end
let!(:step) { described_class.new(pipeline, command) } let!(:step) { described_class.new(pipeline, command) }
......
...@@ -3,10 +3,7 @@ require 'spec_helper' ...@@ -3,10 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
set(:project) { create(:project, :repository) } set(:project) { create(:project, :repository) }
set(:user) { create(:user) } set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
let(:command) do
double('command', project: project, current_user: user)
end
let!(:step) { described_class.new(pipeline, command) } let!(:step) { described_class.new(pipeline, command) }
...@@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do ...@@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
step.perform! step.perform!
end end
context 'when pipeline ref and sha exists' do context 'when ref and sha exists' do
let(:pipeline) do let(:command) do
build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project) Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, origin_ref: 'master', checkout_sha: project.commit.id)
end end
it 'does not break the chain' do it 'does not break the chain' do
...@@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do ...@@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end end
end end
context 'when pipeline ref does not exist' do context 'when ref does not exist' do
let(:pipeline) do let(:command) do
build_stubbed(:ci_pipeline, ref: 'something', project: project) Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, origin_ref: 'something')
end end
it 'breaks the chain' do it 'breaks the chain' do
...@@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do ...@@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end end
end end
context 'when pipeline does not have SHA set' do context 'when does not have existing SHA set' do
let(:pipeline) do let(:command) do
build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project) Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, origin_ref: 'master', checkout_sha: 'something')
end end
it 'breaks the chain' do it 'breaks the chain' do
......
...@@ -193,7 +193,15 @@ describe Gitlab::GitAccess do ...@@ -193,7 +193,15 @@ describe Gitlab::GitAccess do
let(:actor) { build(:rsa_deploy_key_2048, user: user) } let(:actor) { build(:rsa_deploy_key_2048, user: user) }
end end
describe '#check_project_moved!' do shared_examples 'check_project_moved' do
it 'enqueues a redirected message' do
push_access_check
expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil
end
end
describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
before do before do
project.add_master(user) project.add_master(user)
end end
...@@ -207,7 +215,40 @@ describe Gitlab::GitAccess do ...@@ -207,7 +215,40 @@ describe Gitlab::GitAccess do
end end
end end
context 'when a redirect was followed to find the project' do context 'when a permanent redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
before do
allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
end
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
end
end
it_behaves_like 'check_project_moved'
end
context 'with a permanent redirect and http protocol' do
let(:redirected_path) { 'some/other-path' }
let(:protocol) { 'http' }
before do
allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
end
it 'allows_push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
end
end
it_behaves_like 'check_project_moved'
end
context 'with a temporal redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' } let(:redirected_path) { 'some/other-path' }
it 'blocks push and pull access' do it 'blocks push and pull access' do
...@@ -219,16 +260,15 @@ describe Gitlab::GitAccess do ...@@ -219,16 +260,15 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/) expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
end end
end end
end
context 'http protocol' do context 'with a temporal redirect and http protocol' do
let(:protocol) { 'http' } let(:redirected_path) { 'some/other-path' }
let(:protocol) { 'http' }
it 'includes the path to the project using HTTP' do it 'does not allow to push and pull access' do
aggregate_failures do expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/) expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
end
end
end end
end end
end end
......
...@@ -70,6 +70,10 @@ describe Gitlab::Identifier do ...@@ -70,6 +70,10 @@ describe Gitlab::Identifier do
expect(identifier.identify_using_commit(project, '123')).to eq(user) expect(identifier.identify_using_commit(project, '123')).to eq(user)
end end
end end
it 'returns nil if the project & ref are not present' do
expect(identifier.identify_using_commit(nil, nil)).to be_nil
end
end end
describe '#identify_using_user' do describe '#identify_using_user' do
......
...@@ -696,4 +696,34 @@ describe Namespace do ...@@ -696,4 +696,34 @@ describe Namespace do
expect(very_deep_nested_group.root_ancestor).to eq(root_group) expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end end
end end
describe "#allowed_path_by_redirects" do
let(:namespace1) { create(:namespace, path: 'foo') }
context "when the path has been taken before" do
before do
namespace1.path = 'bar'
namespace1.save!
end
it 'should be invalid' do
namespace2 = build(:group, path: 'foo')
expect(namespace2).to be_invalid
end
it 'should return an error on path' do
namespace2 = build(:group, path: 'foo')
namespace2.valid?
expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one')
end
end
context "when the path has not been taken before" do
it 'should be valid' do
expect(RedirectRoute.count).to eq(0)
namespace = build(:namespace)
expect(namespace).to be_valid
end
end
end
end end
...@@ -87,6 +87,7 @@ describe Route do ...@@ -87,6 +87,7 @@ describe Route do
end end
context 'when conflicting redirects exist' do context 'when conflicting redirects exist' do
let(:route) { create(:project).route }
let!(:conflicting_redirect1) { route.create_redirect('bar/test') } let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') } let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') } let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
...@@ -141,11 +142,50 @@ describe Route do ...@@ -141,11 +142,50 @@ describe Route do
expect(redirect_route.source).to eq(route.source) expect(redirect_route.source).to eq(route.source)
expect(redirect_route.path).to eq('foo') expect(redirect_route.path).to eq('foo')
end end
context 'when the source is a Project' do
it 'creates a temporal RedirectRoute' do
project = create(:project)
route = project.route
redirect_route = route.create_redirect('foo')
expect(redirect_route.permanent?).to be_falsy
end
end
context 'when the source is not a project' do
it 'creates a permanent RedirectRoute' do
redirect_route = route.create_redirect('foo', permanent: true)
expect(redirect_route.permanent?).to be_truthy
end
end
end end
describe '#delete_conflicting_redirects' do describe '#delete_conflicting_redirects' do
context 'with permanent redirect' do
it 'does not delete the redirect' do
route.create_redirect("#{route.path}/foo", permanent: true)
expect do
route.delete_conflicting_redirects
end.not_to change { RedirectRoute.count }
end
end
context 'with temporal redirect' do
let(:route) { create(:project).route }
it 'deletes the redirect' do
route.create_redirect("#{route.path}/foo")
expect do
route.delete_conflicting_redirects
end.to change { RedirectRoute.count }.by(-1)
end
end
context 'when a redirect route with the same path exists' do context 'when a redirect route with the same path exists' do
context 'when the redirect route has matching case' do context 'when the redirect route has matching case' do
let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path) } let!(:redirect1) { route.create_redirect(route.path) }
it 'deletes the redirect' do it 'deletes the redirect' do
...@@ -169,6 +209,7 @@ describe Route do ...@@ -169,6 +209,7 @@ describe Route do
end end
context 'when the redirect route is differently cased' do context 'when the redirect route is differently cased' do
let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path.upcase) } let!(:redirect1) { route.create_redirect(route.path.upcase) }
it 'deletes the redirect' do it 'deletes the redirect' do
...@@ -185,7 +226,32 @@ describe Route do ...@@ -185,7 +226,32 @@ describe Route do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end end
context 'with permanent redirects' do
it 'does not return anything' do
route.create_redirect("#{route.path}/foo", permanent: true)
route.create_redirect("#{route.path}/foo/bar", permanent: true)
route.create_redirect("#{route.path}/baz/quz", permanent: true)
expect(route.conflicting_redirects).to be_empty
end
end
context 'with temporal redirects' do
let(:route) { create(:project).route }
it 'returns the redirect routes' do
route = create(:project).route
redirect1 = route.create_redirect("#{route.path}/foo")
redirect2 = route.create_redirect("#{route.path}/foo/bar")
redirect3 = route.create_redirect("#{route.path}/baz/quz")
expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
end
end
context 'when a redirect route with the same path exists' do context 'when a redirect route with the same path exists' do
let(:route) { create(:project).route }
context 'when the redirect route has matching case' do context 'when the redirect route has matching case' do
let!(:redirect1) { route.create_redirect(route.path) } let!(:redirect1) { route.create_redirect(route.path) }
...@@ -214,4 +280,42 @@ describe Route do ...@@ -214,4 +280,42 @@ describe Route do
end end
end end
end end
describe "#conflicting_redirect_exists?" do
context 'when a conflicting redirect exists' do
let(:group1) { create(:group, path: 'foo') }
let(:group2) { create(:group, path: 'baz') }
it 'should not be saved' do
group1.path = 'bar'
group1.save
group2.path = 'foo'
expect(group2.save).to be_falsy
end
it 'should return an error on path' do
group1.path = 'bar'
group1.save
group2.path = 'foo'
group2.valid?
expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one')
end
end
context 'when a conflicting redirect does not exist' do
let(:project1) { create(:project, path: 'foo') }
let(:project2) { create(:project, path: 'baz') }
it 'should be saved' do
project1.path = 'bar'
project1.save
project2.path = 'foo'
expect(project2.save).to be_truthy
end
end
end
end end
...@@ -2715,4 +2715,28 @@ describe User do ...@@ -2715,4 +2715,28 @@ describe User do
include_examples 'max member access for groups' include_examples 'max member access for groups'
end end
end end
describe "#username_previously_taken?" do
let(:user1) { create(:user, username: 'foo') }
context 'when the username has been taken before' do
before do
user1.username = 'bar'
user1.save!
end
it 'should raise an ActiveRecord::RecordInvalid exception' do
user2 = build(:user, username: 'foo')
expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/)
end
end
context 'when the username has not been taken before' do
it 'should be valid' do
expect(RedirectRoute.count).to eq(0)
user2 = build(:user, username: 'baz')
expect(user2).to be_valid
end
end
end
end end
...@@ -585,16 +585,7 @@ describe API::Internal do ...@@ -585,16 +585,7 @@ describe API::Internal do
context 'the project path was changed' do context 'the project path was changed' do
let!(:old_path_to_repo) { project.repository.path_to_repo } let!(:old_path_to_repo) { project.repository.path_to_repo }
let!(:old_full_path) { project.full_path } let!(:repository) { project.repository }
let(:project_moved_message) do
<<-MSG.strip_heredoc
Project '#{old_full_path}' was moved to '#{project.full_path}'.
Please update your Git remote and try again:
git remote set-url origin #{project.ssh_url_to_repo}
MSG
end
before do before do
project.team << [user, :developer] project.team << [user, :developer]
...@@ -603,19 +594,17 @@ describe API::Internal do ...@@ -603,19 +594,17 @@ describe API::Internal do
end end
it 'rejects the push' do it 'rejects the push' do
push_with_path(key, old_path_to_repo) push(key, project)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to be_falsey expect(json_response['status']).to be_falsy
expect(json_response['message']).to eq(project_moved_message)
end end
it 'rejects the SSH pull' do it 'rejects the SSH pull' do
pull_with_path(key, old_path_to_repo) pull(key, project)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to be_falsey expect(json_response['status']).to be_falsy
expect(json_response['message']).to eq(project_moved_message)
end end
end end
end end
...@@ -743,7 +732,7 @@ describe API::Internal do ...@@ -743,7 +732,7 @@ describe API::Internal do
# end # end
# end # end
describe 'POST /internal/post_receive' do describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
let(:identifier) { 'key-123' } let(:identifier) { 'key-123' }
let(:valid_params) do let(:valid_params) do
...@@ -761,6 +750,8 @@ describe API::Internal do ...@@ -761,6 +750,8 @@ describe API::Internal do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
allow(described_class).to receive(:identify).and_return(user)
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
end end
it 'enqueues a PostReceive worker job' do it 'enqueues a PostReceive worker job' do
...@@ -828,6 +819,19 @@ describe API::Internal do ...@@ -828,6 +819,19 @@ describe API::Internal do
expect(json_response['broadcast_message']).to eq(nil) expect(json_response['broadcast_message']).to eq(nil)
end end
end end
context 'with a redirected data' do
it 'returns redirected message on the response' do
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http')
project_moved.add_redirect_message
post api("/internal/post_receive"), valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["redirected_message"]).to be_present
expect(json_response["redirected_message"]).to eq(project_moved.redirect_message)
end
end
end end
describe 'POST /internal/pre_receive' do describe 'POST /internal/pre_receive' do
......
...@@ -324,9 +324,9 @@ describe 'Git HTTP requests' do ...@@ -324,9 +324,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc <<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'. Project '#{redirect.path}' was moved to '#{project.full_path}'.
Please update your Git remote and try again: Please update your Git remote:
git remote set-url origin #{project.http_url_to_repo} git remote set-url origin #{project.http_url_to_repo} and try again.
MSG MSG
end end
...@@ -533,9 +533,9 @@ describe 'Git HTTP requests' do ...@@ -533,9 +533,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc <<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'. Project '#{redirect.path}' was moved to '#{project.full_path}'.
Please update your Git remote and try again: Please update your Git remote:
git remote set-url origin #{project.http_url_to_repo} git remote set-url origin #{project.http_url_to_repo} and try again.
MSG MSG
end end
......
...@@ -499,5 +499,20 @@ describe Ci::CreatePipelineService do ...@@ -499,5 +499,20 @@ describe Ci::CreatePipelineService do
end end
end end
end end
context 'when pipeline is running for a tag' do
before do
config = YAML.dump(test: { script: 'test', only: ['branches'] },
deploy: { script: 'deploy', only: ['tags'] })
stub_ci_pipeline_yaml_file(config)
end
it 'creates a tagged pipeline' do
pipeline = execute_service(ref: 'v1.0.0')
expect(pipeline.tag?).to be true
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment