Commit 4d1bcae5 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents f1803186 39e52710
...@@ -380,10 +380,10 @@ GEM ...@@ -380,10 +380,10 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.6.6) grpc (1.7.2)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1) googleauth (>= 0.5.1, < 0.7)
gssapi (1.2.0) gssapi (1.2.0)
ffi (>= 1.0.1) ffi (>= 1.0.1)
haml (4.0.7) haml (4.0.7)
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */ /* global EditBlob */
/* global NewCommitForm */ import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob'; import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone'; import BlobFileDropzone from '../blob/blob_file_dropzone';
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import Flash from '../flash'; import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards'; import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub'; import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import './models/issue'; import './models/issue';
import './models/label'; import './models/label';
import './models/list'; import './models/list';
...@@ -15,7 +16,7 @@ import './models/project'; ...@@ -15,7 +16,7 @@ import './models/project';
import './models/assignee'; import './models/assignee';
import './stores/boards_store'; import './stores/boards_store';
import './stores/modal_store'; import './stores/modal_store';
import './services/board_service'; import BoardService from './services/board_service';
import './mixins/modal_mixins'; import './mixins/modal_mixins';
import './mixins/sortable_default_options'; import './mixins/sortable_default_options';
import './filters/due_date_filters'; import './filters/due_date_filters';
...@@ -84,11 +85,16 @@ $(() => { ...@@ -84,11 +85,16 @@ $(() => {
}); });
Store.rootPath = this.boardsEndpoint; Store.rootPath = this.boardsEndpoint;
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
...@@ -120,6 +126,46 @@ $(() => { ...@@ -120,6 +126,46 @@ $(() => {
methods: { methods: {
updateTokens() { updateTokens() {
this.filterManager.updateTokens(); this.filterManager.updateTokens();
},
updateDetailIssue(newIssue) {
const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.json())
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
subscribed: data.subscribed,
});
})
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
Flash(__('An error occurred while fetching sidebar data'));
});
}
Store.detail.issue = newIssue;
},
clearDetailIssue() {
Store.detail.issue = {};
},
toggleSubscription(id) {
const issue = Store.detail.issue;
if (issue.id === id && issue.toggleSubscriptionEndpoint) {
issue.setFetchingState('subscriptions', true);
BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
.then(() => {
issue.setFetchingState('subscriptions', false);
issue.updateData({
subscribed: !issue.subscribed,
});
})
.catch(() => {
issue.setFetchingState('subscriptions', false);
Flash(__('An error occurred when toggling the notification subscription'));
});
}
} }
}, },
}); });
......
<script>
import './issue_card_inner'; import './issue_card_inner';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardsIssueCard', name: 'BoardsIssueCard',
template: `
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true" />
</li>
`,
components: { components: {
'issue-card-inner': gl.issueBoards.IssueCardInner, 'issue-card-inner': gl.issueBoards.IssueCardInner,
}, },
...@@ -58,12 +43,31 @@ export default { ...@@ -58,12 +43,31 @@ export default {
this.showDetail = false; this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {}; eventHub.$emit('clearDetailIssue');
} else { } else {
Store.detail.issue = this.issue; eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list; Store.detail.list = this.list;
} }
} }
}, },
}, },
}; };
</script>
<template>
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true" />
</li>
</template>
/* global Sortable */ /* global Sortable */
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../flash'; import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees'; import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select'; import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors(); new DueDateSelectors();
new LabelsSelect(); new LabelsSelect();
new Sidebar(); new Sidebar();
gl.Subscription.bindAll('.subscription');
}, },
components: { components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn, removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle, subscriptions,
assignees: Assignees,
}, },
}); });
...@@ -18,6 +18,11 @@ class ListIssue { ...@@ -18,6 +18,11 @@ class ListIssue {
this.assignees = []; this.assignees = [];
this.selected = false; this.selected = false;
this.position = obj.relative_position || Infinity; this.position = obj.relative_position || Infinity;
this.isFetching = {
subscriptions: true,
};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
this.weight = obj.weight; this.weight = obj.weight;
...@@ -81,6 +86,14 @@ class ListIssue { ...@@ -81,6 +86,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
} }
updateData(newData) {
Object.assign(this, newData);
}
setFetchingState(key, value) {
this.isFetching[key] = value;
}
update (url) { update (url) {
const data = { const data = {
issue: { issue: {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
class BoardService { export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: { issues: {
...@@ -117,6 +117,14 @@ class BoardService { ...@@ -117,6 +117,14 @@ class BoardService {
return this.issues.bulkUpdate(data); return this.issues.bulkUpdate(data);
} }
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
}
} }
window.BoardService = BoardService; window.BoardService = BoardService;
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
import { s__ } from './locale'; import { s__ } from './locale';
import projectSelect from './project_select'; import projectSelect from './project_select';
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ import Milestone from './milestone';
import IssuableForm from './issuable_form'; import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select'; import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global NewBranchForm */ import NewBranchForm from './new_branch_form';
/* global NotificationsForm */ /* global NotificationsForm */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
import groupAvatar from './group_avatar'; import groupAvatar from './group_avatar';
...@@ -18,8 +18,7 @@ import groupsSelect from './groups_select'; ...@@ -18,8 +18,7 @@ import groupsSelect from './groups_select';
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
import NamespaceSelect from './namespace_select'; import NamespaceSelect from './namespace_select';
/* global NewCommitForm */ import NewCommitForm from './new_commit_form';
/* global NewBranchForm */
import Project from './project'; import Project from './project';
import projectAvatar from './project_avatar'; import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
......
...@@ -16,7 +16,6 @@ export default () => { ...@@ -16,7 +16,6 @@ export default () => {
new LabelsSelect(); new LabelsSelect();
new WeightSelect(); new WeightSelect();
new IssuableContext(sidebarOptions.currentUser); new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new DueDateSelectors(); new DueDateSelectors();
window.sidebar = new Sidebar(); window.sidebar = new Sidebar();
}; };
/* eslint-disable no-new */ /* eslint-disable no-new */
import LabelsSelect from './labels_select'; import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global SubscriptionSelect */
/* global WeightSelect */ /* global WeightSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select'; import issueStatusSelect from './issue_status_select';
...@@ -12,6 +11,6 @@ export default () => { ...@@ -12,6 +11,6 @@ export default () => {
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
issueStatusSelect(); issueStatusSelect();
new SubscriptionSelect(); subscriptionSelect();
new WeightSelect(); new WeightSelect();
}; };
/* eslint-disable class-methods-use-this, no-new */ /* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select'; import './milestone_select';
import issueStatusSelect from './issue_status_select'; import issueStatusSelect from './issue_status_select';
import './subscription_select'; import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select'; import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
...@@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
issueStatusSelect(); issueStatusSelect();
new SubscriptionSelect(); subscriptionSelect();
} }
setupBulkUpdateActions() { setupBulkUpdateActions() {
......
...@@ -34,6 +34,11 @@ export default { ...@@ -34,6 +34,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -234,6 +239,7 @@ export default { ...@@ -234,6 +239,7 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
/> />
<div v-else> <div v-else>
<title-component <title-component
......
...@@ -17,6 +17,11 @@ ...@@ -17,6 +17,11 @@
type: String, type: String,
required: true, required: true,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
markdownField, markdownField,
...@@ -36,7 +41,8 @@ ...@@ -36,7 +41,8 @@
</label> </label>
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"> :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile">
<textarea <textarea
id="issue-description" id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
......
...@@ -41,6 +41,11 @@ ...@@ -41,6 +41,11 @@
required: false, required: false,
default: true, default: true,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
...@@ -83,7 +88,8 @@ ...@@ -83,7 +88,8 @@
<description-field <description-field
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" :can-destroy="canDestroy"
......
...@@ -61,11 +61,7 @@ import './line_highlighter'; ...@@ -61,11 +61,7 @@ import './line_highlighter';
import initLogoAnimation from './logo'; import initLogoAnimation from './logo';
import './merge_request'; import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone';
import './milestone_select'; import './milestone_select';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
import './notes'; import './notes';
import './notifications_dropdown'; import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
...@@ -81,9 +77,6 @@ import './render_gfm'; ...@@ -81,9 +77,6 @@ import './render_gfm';
import './right_sidebar'; import './right_sidebar';
import './search'; import './search';
import './search_autocomplete'; import './search_autocomplete';
import './smart_interval';
import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
// EE-only scripts // EE-only scripts
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Sortable */ /* global Sortable */
import Flash from './flash'; import Flash from './flash';
(function() { export default class Milestone {
this.Milestone = (function() { constructor() {
function Milestone() { this.bindTabsSwitching();
this.bindTabsSwitching();
// Load merge request tab if it is active // Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend // merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a')); this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab(); this.loadInitialTab();
} }
bindTabsSwitching() {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
Milestone.prototype.bindTabsSwitching = function() { location.hash = $target.attr('href');
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { this.loadTab($target);
const $target = $(e.target); });
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
location.hash = $target.attr('href'); if ($target.length) {
this.loadTab($target); $target.tab('show');
}
}
// eslint-disable-next-line class-methods-use-this
loadTab($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
}); });
}; }
}
Milestone.prototype.loadInitialTab = function() { }
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
}
};
return Milestone;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
import RefSelectDropdown from '~/ref_select_dropdown'; import RefSelectDropdown from './ref_select_dropdown';
(function() { export default class NewBranchForm {
this.NewBranchForm = (function() { constructor(form, availableRefs) {
function NewBranchForm(form, availableRefs) { this.validate = this.validate.bind(this);
this.validate = this.validate.bind(this); this.branchNameError = form.find('.js-branch-name-error');
this.branchNameError = form.find('.js-branch-name-error'); this.name = form.find('.js-branch-name');
this.name = form.find('.js-branch-name'); this.ref = form.find('#ref');
this.ref = form.find('#ref'); new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new this.setupRestrictions();
this.setupRestrictions(); this.addBinding();
this.addBinding(); this.init();
this.init(); }
addBinding() {
return this.name.on('blur', this.validate);
}
init() {
if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
} }
}
NewBranchForm.prototype.addBinding = function() { setupRestrictions() {
return this.name.on('blur', this.validate); var endsWith, invalid, single, startsWith;
startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: "or"
}; };
endsWith = {
NewBranchForm.prototype.init = function() { pattern: /(\/|\.|\.lock)$/g,
if (this.name.length && this.name.val().length > 0) { prefix: "can't end in",
return this.name.trigger('blur'); conjunction: "or"
}
}; };
invalid = {
NewBranchForm.prototype.setupRestrictions = function() { pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
var endsWith, invalid, single, startsWith; prefix: "can't contain",
startsWith = { conjunction: ", "
pattern: /^(\/|\.)/g, };
prefix: "can't start with", single = {
conjunction: "or" pattern: /^@+$/g,
}; prefix: "can't be",
endsWith = { conjunction: "or"
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: "or"
};
invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
prefix: "can't contain",
conjunction: ", "
};
single = {
pattern: /^@+$/g,
prefix: "can't be",
conjunction: "or"
};
return this.restrictions = [startsWith, invalid, endsWith, single];
}; };
return this.restrictions = [startsWith, invalid, endsWith, single];
}
NewBranchForm.prototype.validate = function() { validate() {
var errorMessage, errors, formatter, unique, validator; var errorMessage, errors, formatter, unique, validator;
const indexOf = [].indexOf; const indexOf = [].indexOf;
this.branchNameError.empty(); this.branchNameError.empty();
unique = function(values, value) { unique = function(values, value) {
if (indexOf.call(values, value) === -1) { if (indexOf.call(values, value) === -1) {
values.push(value); values.push(value);
}
return values;
};
formatter = function(values, restriction) {
var formatted;
formatted = values.map(function(value) {
switch (false) {
case !/\s/.test(value):
return 'spaces';
case !/\/{2,}/g.test(value):
return 'consecutive slashes';
default:
return "'" + value + "'";
}
});
return restriction.prefix + " " + (formatted.join(restriction.conjunction));
};
validator = (function(_this) {
return function(errors, restriction) {
var matched;
matched = _this.name.val().match(restriction.pattern);
if (matched) {
return errors.concat(formatter(matched.reduce(unique, []), restriction));
} else {
return errors;
}
};
})(this);
errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
errorMessage = $("<span/>").text(errors.join(', '));
return this.branchNameError.append(errorMessage);
} }
return values;
}; };
formatter = function(values, restriction) {
return NewBranchForm; var formatted;
})(); formatted = values.map(function(value) {
}).call(window); switch (false) {
case !/\s/.test(value):
return 'spaces';
case !/\/{2,}/g.test(value):
return 'consecutive slashes';
default:
return "'" + value + "'";
}
});
return restriction.prefix + " " + (formatted.join(restriction.conjunction));
};
validator = (function(_this) {
return function(errors, restriction) {
var matched;
matched = _this.name.val().match(restriction.pattern);
if (matched) {
return errors.concat(formatter(matched.reduce(unique, []), restriction));
} else {
return errors;
}
};
})(this);
errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
errorMessage = $("<span/>").text(errors.join(', '));
return this.branchNameError.append(errorMessage);
}
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() { export default class NewCommitForm {
this.NewCommitForm = (function() { constructor(form) {
function NewCommitForm(form) { this.form = form;
this.form = form; this.renderDestination = this.renderDestination.bind(this);
this.renderDestination = this.renderDestination.bind(this); this.branchName = form.find('.js-branch-name');
this.branchName = form.find('.js-branch-name'); this.originalBranch = form.find('.js-original-branch');
this.originalBranch = form.find('.js-original-branch'); this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); this.branchName.keyup(this.renderDestination);
this.branchName.keyup(this.renderDestination); this.renderDestination();
this.renderDestination(); }
}
NewCommitForm.prototype.renderDestination = function() { renderDestination() {
var different; var different;
different = this.branchName.val() !== this.originalBranch.val(); different = this.branchName.val() !== this.originalBranch.val();
if (different) { if (different) {
this.createMergeRequestContainer.show(); this.createMergeRequestContainer.show();
if (!this.wasDifferent) { if (!this.wasDifferent) {
this.createMergeRequest.prop('checked', true); this.createMergeRequest.prop('checked', true);
}
} else {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
} }
return this.wasDifferent = different; } else {
}; this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
return NewCommitForm; }
})(); return this.wasDifferent = different;
}).call(window); }
}
...@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store'; ...@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator'; import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
export default { export default {
...@@ -21,7 +22,7 @@ export default { ...@@ -21,7 +22,7 @@ export default {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
.catch(() => { .catch(() => {
Flash('Error occurred when toggling the notification subscription'); Flash(__('Error occurred when toggling the notification subscription'));
}); });
}, },
}, },
......
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
}, },
id: {
type: Number,
required: false,
},
}, },
components: { components: {
loadingButton, loadingButton,
...@@ -32,7 +36,7 @@ export default { ...@@ -32,7 +36,7 @@ export default {
}, },
methods: { methods: {
toggleSubscription() { toggleSubscription() {
eventHub.$emit('toggleSubscription'); eventHub.$emit('toggleSubscription', this.id);
}, },
}, },
}; };
......
class Subscription {
constructor(containerElm) {
this.containerElm = containerElm;
const subscribeButton = containerElm.querySelector('.js-subscribe-button');
if (subscribeButton) {
// remove class so we don't bind twice
subscribeButton.classList.remove('js-subscribe-button');
subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
}
}
toggleSubscription(event) {
const button = event.currentTarget;
const buttonSpan = button.querySelector('span');
if (!buttonSpan || button.classList.contains('disabled')) {
return;
}
button.classList.add('disabled');
// hack to allow this to work with the issue boards Vue object
const isBoardsPage = document.querySelector('html').classList.contains('issue-boards-page');
const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
let toggleActionUrl = this.containerElm.dataset.url;
if (isBoardsPage) {
toggleActionUrl = toggleActionUrl.replace(':project_path', gl.issueBoards.BoardsStore.detail.issue.project.path);
}
$.post(toggleActionUrl, () => {
button.classList.remove('disabled');
if (isBoardsPage) {
gl.issueBoards.boardStoreIssueSet(
'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed,
);
} else {
buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
}
});
}
static bindAll(selector) {
[].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
}
}
window.gl = window.gl || {};
window.gl.Subscription = Subscription;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
const fieldName = $(element).data('field-name');
class SubscriptionSelect { return $(element).glDropdown({
constructor() { selectable: true,
$('.js-subscription-event').each(function(i, el) { fieldName,
var fieldName; toggleLabel(selected, el, instance) {
fieldName = $(el).data("field-name"); let label = 'Subscription';
return $(el).glDropdown({ const $item = instance.dropdown.find('.is-active');
selectable: true, if ($item.length) {
fieldName: fieldName, label = $item.text();
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Subscription';
$item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
} }
}); return label;
},
clicked(options) {
return options.e.preventDefault();
},
id(obj, el) {
return $(el).data('id');
},
}); });
} });
} }
window.SubscriptionSelect = SubscriptionSelect;
...@@ -6,10 +6,9 @@ ...@@ -6,10 +6,9 @@
Sample configuration: Sample configuration:
<icon <icon
:img-src="userAvatarSrc" name="retry"
:img-alt="tooltipText" :size="32"
:tooltip-text="tooltipText" css-classes="top"
tooltip-placement="top"
/> />
*/ */
......
...@@ -25,6 +25,11 @@ ...@@ -25,6 +25,11 @@
type: String, type: String,
required: false, required: false,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -129,6 +134,7 @@ ...@@ -129,6 +134,7 @@
<markdown-toolbar <markdown-toolbar
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/> />
</div> </div>
</div> </div>
......
...@@ -9,6 +9,11 @@ ...@@ -9,6 +9,11 @@
type: String, type: String,
required: false, required: false,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
}, },
}; };
</script> </script>
...@@ -41,7 +46,10 @@ ...@@ -41,7 +46,10 @@
are supported are supported
</template> </template>
</div> </div>
<span class="uploading-container"> <span
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-progress-container hide"> <span class="uploading-progress-container hide">
<i <i
class="fa fa-file-image-o toolbar-button-icon" class="fa fa-file-image-o toolbar-button-icon"
......
...@@ -86,6 +86,7 @@ module Boards ...@@ -86,6 +86,7 @@ module Boards
resource.as_json( resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true, labels: true,
sidebar_endpoints: true,
include: { include: {
project: { only: [:id, :path] }, project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
...@@ -45,7 +45,7 @@ module KerberosSpnegoHelper ...@@ -45,7 +45,7 @@ module KerberosSpnegoHelper
krb_principal = spnego_credentials!(spnego_token) krb_principal = spnego_credentials!(spnego_token)
return unless krb_principal return unless krb_principal
identity = ::Identity.find_by(provider: :kerberos, extern_uid: krb_principal) identity = ::Identity.with_extern_uid(:kerberos, krb_principal).take
identity&.user identity&.user
end end
......
...@@ -26,6 +26,10 @@ module Geo ...@@ -26,6 +26,10 @@ module Geo
class_name: 'Geo::HashedStorageMigratedEvent', class_name: 'Geo::HashedStorageMigratedEvent',
foreign_key: :hashed_storage_migrated_event_id foreign_key: :hashed_storage_migrated_event_id
belongs_to :lfs_object_deleted_event,
class_name: 'Geo::LfsObjectDeletedEvent',
foreign_key: :lfs_object_deleted_event_id
def self.latest_event def self.latest_event
order(id: :desc).first order(id: :desc).first
end end
...@@ -36,7 +40,8 @@ module Geo ...@@ -36,7 +40,8 @@ module Geo
repository_deleted_event || repository_deleted_event ||
repository_renamed_event || repository_renamed_event ||
repositories_changed_event || repositories_changed_event ||
hashed_storage_migrated_event hashed_storage_migrated_event ||
lfs_object_deleted_event
end end
def project_id def project_id
......
module Geo
class LfsObjectDeletedEvent < ActiveRecord::Base
include Geo::Model
belongs_to :lfs_object
validates :lfs_object, :oid, :file_path, presence: true
end
end
...@@ -6,11 +6,8 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -6,11 +6,8 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
scope :dirty, -> { where(arel_table[:resync_repository].eq(true).or(arel_table[:resync_wiki].eq(true))) } scope :dirty, -> { where(arel_table[:resync_repository].eq(true).or(arel_table[:resync_wiki].eq(true))) }
def self.failed def self.failed
repository_sync_failed = arel_table[:last_repository_synced_at].not_eq(nil) repository_sync_failed = arel_table[:repository_retry_count].gt(0)
.and(arel_table[:last_repository_successful_sync_at].eq(nil)) wiki_sync_failed = arel_table[:wiki_retry_count].gt(0)
wiki_sync_failed = arel_table[:last_wiki_synced_at].not_eq(nil)
.and(arel_table[:last_wiki_successful_sync_at].eq(nil))
where(repository_sync_failed.or(wiki_sync_failed)) where(repository_sync_failed.or(wiki_sync_failed))
end end
......
class Identity < ActiveRecord::Base class Identity < ActiveRecord::Base
prepend EE::Identity
include Sortable include Sortable
include CaseSensitivity include CaseSensitivity
......
...@@ -285,7 +285,12 @@ class Issue < ActiveRecord::Base ...@@ -285,7 +285,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {}) def as_json(options = {})
super(options).tap do |json| super(options).tap do |json|
json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user] if options.key?(:sidebar_endpoints) && project
url_helper = Gitlab::Routing.url_helpers
json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
end
if options.key?(:labels) if options.key?(:labels)
json[:labels] = labels.as_json( json[:labels] = labels.as_json(
......
class LfsObject < ActiveRecord::Base class LfsObject < ActiveRecord::Base
prepend EE::LfsObject
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects has_many :projects, through: :lfs_objects_projects
......
...@@ -8,6 +8,7 @@ module Geo ...@@ -8,6 +8,7 @@ module Geo
class BaseSyncService class BaseSyncService
include ExclusiveLeaseGuard include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers include ::Gitlab::Geo::ProjectLogHelpers
include ::Gitlab::ShellAdapter
include Delay include Delay
class << self class << self
...@@ -136,7 +137,7 @@ module Geo ...@@ -136,7 +137,7 @@ module Geo
if started_at if started_at
attrs["last_#{type}_synced_at"] = started_at attrs["last_#{type}_synced_at"] = started_at
attrs["#{type}_retry_count"] = retry_count + 1 attrs["#{type}_retry_count"] = retry_count + 1
attrs["#{type}_retry_at"] = Time.now + delay(attrs["#{type}_retry_count"]).seconds attrs["#{type}_retry_at"] = next_retry_time(attrs["#{type}_retry_count"])
end end
if finished_at if finished_at
...@@ -173,13 +174,17 @@ module Geo ...@@ -173,13 +174,17 @@ module Geo
registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend
end end
def random_disk_path(prefix)
random_string = SecureRandom.hex(7)
"#{repository.disk_path}_#{prefix}#{random_string}"
end
def disk_path_temp def disk_path_temp
unless @disk_path_temp @disk_path_temp ||= random_disk_path('')
random_string = SecureRandom.hex(7) end
@disk_path_temp = "#{repository.disk_path}_#{random_string}"
end
@disk_path_temp def deleted_disk_path_temp
@deleted_path ||= "#{repository.disk_path}+failed-geo-sync"
end end
def build_temporary_repository def build_temporary_repository
...@@ -199,16 +204,35 @@ module Geo ...@@ -199,16 +204,35 @@ module Geo
"Setting newly downloaded repository as main", "Setting newly downloaded repository as main",
storage_path: project.repository_storage_path, storage_path: project.repository_storage_path,
temp_path: disk_path_temp, temp_path: disk_path_temp,
deleted_disk_path_temp: deleted_disk_path_temp,
disk_path: repository.disk_path disk_path: repository.disk_path
) )
unless gitlab_shell.remove_repository(project.repository_storage_path, repository.disk_path) # Remove the deleted path in case it exists, but it may not be there
raise Gitlab::Shell::Error, 'Can not remove outdated main repository to replace it' gitlab_shell.remove_repository(project.repository_storage_path, deleted_disk_path_temp)
# Move the original repository out of the way
unless gitlab_shell.mv_repository(project.repository_storage_path, repository.disk_path, deleted_disk_path_temp)
raise Gitlab::Shell::Error, 'Can not move original repository out of the way'
end end
unless gitlab_shell.mv_repository(project.repository_storage_path, disk_path_temp, repository.disk_path) unless gitlab_shell.mv_repository(project.repository_storage_path, disk_path_temp, repository.disk_path)
raise Gitlab::Shell::Error, 'Can not move temporary repository' raise Gitlab::Shell::Error, 'Can not move temporary repository'
end end
# Purge the original repository
unless gitlab_shell.remove_repository(project.repository_storage_path, deleted_disk_path_temp)
raise Gitlab::Shell::Error, 'Can not remove outdated main repository'
end
end
# To prevent the retry time from storing invalid dates in the database,
# cap the max time to a week plus some random jitter value.
def next_retry_time(retry_count)
proposed_time = Time.now + delay(retry_count).seconds
max_future_time = Time.now + 7.days + delay(1).seconds
[proposed_time, max_future_time].min
end end
end end
end end
...@@ -34,9 +34,12 @@ module Geo ...@@ -34,9 +34,12 @@ module Geo
return unless Gitlab::Geo.primary? return unless Gitlab::Geo.primary?
return unless Gitlab::Geo.secondary_nodes.any? # no need to create an event if no one is listening return unless Gitlab::Geo.secondary_nodes.any? # no need to create an event if no one is listening
Geo::EventLog.create!("#{self.class.event_type}" => build_event) event = build_event
event.validate!
Geo::EventLog.create!("#{self.class.event_type}" => event)
rescue ActiveRecord::RecordInvalid, NoMethodError => e rescue ActiveRecord::RecordInvalid, NoMethodError => e
log_error("#{self.event_type.to_s.humanize} could not be created", e) log_error("#{self.class.event_type.to_s.humanize} could not be created", e)
end end
private private
......
module Geo
class LfsObjectDeletedEventStore < EventStore
self.event_type = :lfs_object_deleted_event
attr_reader :lfs_object
def initialize(lfs_object)
@lfs_object = lfs_object
end
def create
return unless lfs_object.local_store?
super
end
private
def build_event
Geo::LfsObjectDeletedEvent.new(
lfs_object: lfs_object,
oid: lfs_object.oid,
file_path: relative_file_path
)
end
def local_store_path
Pathname.new(LfsObjectUploader.local_store_path)
end
def relative_file_path
return unless lfs_object.file.present?
Pathname.new(lfs_object.file.path).relative_path_from(local_store_path)
end
# This is called by ProjectLogHelpers to build json log with context info
#
# @see ::Gitlab::Geo::ProjectLogHelpers
def base_log_data(message)
{
class: self.class.name,
lfs_object_id: lfs_object.id,
file_path: lfs_object.file.path,
message: message
}
end
end
end
module Geo module Geo
class RepositorySyncService < BaseSyncService class RepositorySyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :repository self.type = :repository
private private
...@@ -32,10 +30,12 @@ module Geo ...@@ -32,10 +30,12 @@ module Geo
Gitlab::Git::RepositoryMirroring::RemoteError, Gitlab::Git::RepositoryMirroring::RemoteError,
Geo::EmptyCloneUrlPrefixError => e Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing repository', e) log_error('Error syncing repository', e)
registry.increment!(:repository_retry_count)
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e) log_error('Invalid repository', e)
log_info('Setting force_to_redownload flag') log_info('Setting force_to_redownload flag')
registry.update(force_to_redownload_repository: true) registry.update(force_to_redownload_repository: true,
repository_retry_count: retry_count + 1)
log_info('Expiring caches') log_info('Expiring caches')
project.repository.after_create project.repository.after_create
ensure ensure
...@@ -54,5 +54,9 @@ module Geo ...@@ -54,5 +54,9 @@ module Geo
def repository def repository
project.repository project.repository
end end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end end
end end
...@@ -33,9 +33,11 @@ module Geo ...@@ -33,9 +33,11 @@ module Geo
ProjectWiki::CouldNotCreateWikiError, ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing wiki repository', e) log_error('Error syncing wiki repository', e)
registry.increment!(:wiki_retry_count)
rescue Gitlab::Git::Repository::NoRepository => e rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid wiki', e) log_error('Invalid wiki', e)
registry.update(force_to_redownload_wiki: true) registry.update(force_to_redownload_wiki: true,
repository_retry_count: retry_count + 1)
ensure ensure
clean_up_temporary_repository if redownload clean_up_temporary_repository if redownload
end end
...@@ -47,5 +49,9 @@ module Geo ...@@ -47,5 +49,9 @@ module Geo
def repository def repository
project.wiki.repository project.wiki.repository
end end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end end
end end
- disable_key_edit = local_assigns.fetch(:disable_key_edit, false) - disable_key_edit = local_assigns.fetch(:disable_key_edit, false)
= form_errors(geo_node) = form_errors(geo_node)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.checkbox .checkbox
= form.label :primary do = form.label :primary do
= form.check_box :primary = form.check_box :primary
%strong This is a primary node %strong This is a primary node
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) } .form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :clone_protocol, s_('Geo|Repository cloning'), class: 'control-label' = form.label :clone_protocol, s_('Geo|Repository cloning'), class: 'control-label'
......
- if current_user - if current_user
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" } .block.subscriptions
%span.issuable-header-text.hide-collapsed.pull-left %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
Notifications ":subscribed" => "issue.subscribed",
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } ":id" => "issue.id" }
%span
{{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
---
title: Geo - Ensure that LFS object deletions are communicated to the secondary
merge_request:
author:
type: fixed
---
title: Disable file attachments for epics
merge_request:
author:
type: fixed
---
title: Update gitlab.yml.example to match the default settings for Geo sync workers
merge_request: 3488
author:
type: fixed
---
title: Fix in-progress repository syncs counting as failed
merge_request: 3424
author:
type: fixed
---
title: Update Issue Boards to fetch the notification subscription status asynchronously
merge_request:
author:
type: performance
---
title: Clean up schema of the "merge_requests" table
merge_request:
author:
type: other
...@@ -254,12 +254,12 @@ production: &base ...@@ -254,12 +254,12 @@ production: &base
# GitLab Geo repository sync worker # GitLab Geo repository sync worker
# NOTE: This will only take effect if Geo is enabled (secondary nodes only) # NOTE: This will only take effect if Geo is enabled (secondary nodes only)
geo_repository_sync_worker: geo_repository_sync_worker:
cron: "*/5 * * * *" cron: "*/1 * * * *"
# GitLab Geo file download dispatch worker # GitLab Geo file download dispatch worker
# NOTE: This will only take effect if Geo is enabled (secondary nodes only) # NOTE: This will only take effect if Geo is enabled (secondary nodes only)
geo_file_download_dispatch_worker: geo_file_download_dispatch_worker:
cron: "*/10 * * * *" cron: "*/1 * * * *"
registry: registry:
# enabled: true # enabled: true
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_authors
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)')
.where('author_id IS NOT NULL')
end
end
def up
# Replacing the ghost user ID logic would be too complex, hence we don't
# redefine the User model here.
ghost_id = User.select(:id).ghost.id
MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch|
batch.update_all(author_id: ghost_id)
end
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :author_id,
on_delete: :nullify
)
end
def down
remove_foreign_key(:merge_requests, column: :author_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_assignees
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)')
.where('assignee_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch|
batch.update_all(assignee_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :assignee_id,
on_delete: :nullify
)
end
def down
remove_foreign_key(:merge_requests, column: :assignee_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_updaters
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)')
.where('updated_by_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch|
batch.update_all(updated_by_id: nil)
end
add_concurrent_index(
:merge_requests,
:updated_by_id,
where: 'updated_by_id IS NOT NULL'
)
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :updated_by_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :updated_by_id)
remove_concurrent_index(:merge_requests, :updated_by_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_mergers
where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)')
.where('merge_user_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch|
batch.update_all(merge_user_id: nil)
end
add_concurrent_index(
:merge_requests,
:merge_user_id,
where: 'merge_user_id IS NOT NULL'
)
add_concurrent_foreign_key(
:merge_requests,
:users,
column: :merge_user_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :merge_user_id)
remove_concurrent_index(:merge_requests, :merge_user_id)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_source_projects
where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)')
.where('source_project_id IS NOT NULL')
end
end
def up
# We need to allow NULL values so we can nullify the column when the source
# project is removed. We _don't_ want to remove the merge request, instead
# the application will keep them but close them.
change_column_null(:merge_requests, :source_project_id, true)
MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch|
batch.update_all(source_project_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:projects,
column: :source_project_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :source_project_id)
change_column_null(:merge_requests, :source_project_id, false)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
def self.with_orphaned_milestones
where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)')
.where('milestone_id IS NOT NULL')
end
end
def up
MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch|
batch.update_all(milestone_id: nil)
end
add_concurrent_foreign_key(
:merge_requests,
:milestones,
column: :milestone_id,
on_delete: :nullify
)
end
def down
remove_foreign_key_without_error(:merge_requests, column: :milestone_id)
end
end
class CreateGeoLfsObjectDeletedEvents < ActiveRecord::Migration
DOWNTIME = false
def change
create_table :geo_lfs_object_deleted_events, id: :bigserial do |t|
# If a LFS object is deleted, we need to retain this entry
t.references :lfs_object, index: true, foreign_key: false, null: false
t.string :oid, null: false
t.string :file_path, null: false
end
add_column :geo_event_log, :lfs_object_deleted_event_id, :integer, limit: 8
end
end
class AddGeoLfsObjectDeletedEventsForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :geo_event_log, :geo_lfs_object_deleted_events,
column: :lfs_object_deleted_event_id, on_delete: :cascade
end
def down
remove_foreign_key :geo_event_log, column: :lfs_object_deleted_event_id
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: 20171114104051) do ActiveRecord::Schema.define(version: 20171120145444) 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"
...@@ -879,6 +879,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -879,6 +879,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
t.integer "repositories_changed_event_id", limit: 8 t.integer "repositories_changed_event_id", limit: 8
t.integer "repository_created_event_id", limit: 8 t.integer "repository_created_event_id", limit: 8
t.integer "hashed_storage_migrated_event_id", limit: 8 t.integer "hashed_storage_migrated_event_id", limit: 8
t.integer "lfs_object_deleted_event_id", limit: 8
end end
add_index "geo_event_log", ["repositories_changed_event_id"], name: "index_geo_event_log_on_repositories_changed_event_id", using: :btree add_index "geo_event_log", ["repositories_changed_event_id"], name: "index_geo_event_log_on_repositories_changed_event_id", using: :btree
...@@ -901,6 +902,14 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -901,6 +902,14 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_index "geo_hashed_storage_migrated_events", ["project_id"], name: "index_geo_hashed_storage_migrated_events_on_project_id", using: :btree add_index "geo_hashed_storage_migrated_events", ["project_id"], name: "index_geo_hashed_storage_migrated_events_on_project_id", using: :btree
create_table "geo_lfs_object_deleted_events", id: :bigserial, force: :cascade do |t|
t.integer "lfs_object_id", null: false
t.string "oid", null: false
t.string "file_path", null: false
end
add_index "geo_lfs_object_deleted_events", ["lfs_object_id"], name: "index_geo_lfs_object_deleted_events_on_lfs_object_id", using: :btree
create_table "geo_node_namespace_links", force: :cascade do |t| create_table "geo_node_namespace_links", force: :cascade do |t|
t.integer "geo_node_id", null: false t.integer "geo_node_id", null: false
t.integer "namespace_id", null: false t.integer "namespace_id", null: false
...@@ -1366,7 +1375,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1366,7 +1375,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
create_table "merge_requests", force: :cascade do |t| create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false t.string "target_branch", null: false
t.string "source_branch", null: false t.string "source_branch", null: false
t.integer "source_project_id", null: false t.integer "source_project_id"
t.integer "author_id" t.integer "author_id"
t.integer "assignee_id" t.integer "assignee_id"
t.string "title" t.string "title"
...@@ -1409,6 +1418,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1409,6 +1418,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
...@@ -1417,6 +1427,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -1417,6 +1427,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "merge_requests_closing_issues", force: :cascade do |t| create_table "merge_requests_closing_issues", force: :cascade do |t|
t.integer "merge_request_id", null: false t.integer "merge_request_id", null: false
...@@ -2431,6 +2442,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -2431,6 +2442,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_foreign_key "gcp_clusters", "services", on_delete: :nullify add_foreign_key "gcp_clusters", "services", on_delete: :nullify
add_foreign_key "gcp_clusters", "users", on_delete: :nullify add_foreign_key "gcp_clusters", "users", on_delete: :nullify
add_foreign_key "geo_event_log", "geo_hashed_storage_migrated_events", column: "hashed_storage_migrated_event_id", name: "fk_27548c6db3", on_delete: :cascade add_foreign_key "geo_event_log", "geo_hashed_storage_migrated_events", column: "hashed_storage_migrated_event_id", name: "fk_27548c6db3", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_lfs_object_deleted_events", column: "lfs_object_deleted_event_id", name: "fk_d5af95fcd9", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repositories_changed_events", column: "repositories_changed_event_id", name: "fk_4a99ebfd60", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repositories_changed_events", column: "repositories_changed_event_id", name: "fk_4a99ebfd60", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_created_events", column: "repository_created_event_id", name: "fk_9b9afb1916", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_created_events", column: "repository_created_event_id", name: "fk_9b9afb1916", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade
...@@ -2474,7 +2486,13 @@ ActiveRecord::Schema.define(version: 20171114104051) do ...@@ -2474,7 +2486,13 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade
add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify
add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
......
...@@ -62,6 +62,7 @@ Shortcuts to GitLab's most visited docs: ...@@ -62,6 +62,7 @@ Shortcuts to GitLab's most visited docs:
- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages. - [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages.
- [Groups](user/group/index.md): Organize your projects in groups. - [Groups](user/group/index.md): Organize your projects in groups.
- [Subgroups](user/group/subgroups/index.md) - [Subgroups](user/group/subgroups/index.md)
- **(EEU)** [Epics](user/group/epics/index.md)
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards. - [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- **(EES/EEP)** [Advanced Global Search](user/search/advanced_global_search.md): Leverage Elasticsearch for faster, more advanced code search across your entire GitLab instance. - **(EES/EEP)** [Advanced Global Search](user/search/advanced_global_search.md): Leverage Elasticsearch for faster, more advanced code search across your entire GitLab instance.
- **(EES/EEP)** [Advanced Syntax Search](user/search/advanced_search_syntax.md): Use advanced queries for more targeted search results. - **(EES/EEP)** [Advanced Syntax Search](user/search/advanced_search_syntax.md): Use advanced queries for more targeted search results.
......
...@@ -620,6 +620,24 @@ the previous section: ...@@ -620,6 +620,24 @@ the previous section:
the `gitlab` database user the `gitlab` database user
1. [Reconfigure GitLab] for the changes to take effect 1. [Reconfigure GitLab] for the changes to take effect
## Architecture
![PG HA Architecture](pg_ha_architecture.png)
Database nodes run two services besides PostgreSQL
1. Repmgrd -- monitors the cluster and handles failover in case of an issue with the master
The failover consists of
* Selecting a new master for the cluster
* Promoting the new node to master
* Instructing remaining servers to follow the new master node
On failure, the old master node is automatically evicted from the cluster, and should be rejoined manually once recovered.
1. Consul -- Monitors the status of each node in the database cluster, and tracks its health in a service definiton on the consul cluster.
Alongside pgbouncer, there is a consul agent that watches the status of the PostgreSQL service. If that status changes, consul runs a script which updates the configuration and reloads pgbouncer
## Troubleshooting ## Troubleshooting
### Consul and PostgreSQL changes not taking effect. ### Consul and PostgreSQL changes not taking effect.
......
...@@ -268,10 +268,9 @@ The prerequisites for a HA Redis setup are the following: ...@@ -268,10 +268,9 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents: 1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby ```ruby
# Enable the master role and disable all other services in the machine # Specify server role as 'redis_master_role'
# (you can still enable Sentinel). roles ['redis_master_role']
redis_master_role['enable'] = true
# IP address pointing to a local IP that the other machines can reach to. # IP address pointing to a local IP that the other machines can reach to.
# You can also set bind to '0.0.0.0' which listen in all interfaces. # You can also set bind to '0.0.0.0' which listen in all interfaces.
# If you really need to bind to an external accessible IP, make # If you really need to bind to an external accessible IP, make
...@@ -285,6 +284,7 @@ The prerequisites for a HA Redis setup are the following: ...@@ -285,6 +284,7 @@ The prerequisites for a HA Redis setup are the following:
# Set up password authentication for Redis (use the same password in all nodes). # Set up password authentication for Redis (use the same password in all nodes).
redis['password'] = 'redis-password-goes-here' redis['password'] = 'redis-password-goes-here'
``` ```
1. Only the primary GitLab application server should handle migrations. To 1. Only the primary GitLab application server should handle migrations. To
prevent database migrations from running on upgrade, add the following prevent database migrations from running on upgrade, add the following
...@@ -296,6 +296,10 @@ The prerequisites for a HA Redis setup are the following: ...@@ -296,6 +296,10 @@ The prerequisites for a HA Redis setup are the following:
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect. 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
> Note: You can specify multiple roles like sentinel and redis as:
> roles ['redis_sentinel_role', 'redis_master_role']. Read more about high
> availability roles at https://docs.gitlab.com/omnibus/roles/
### Step 2. Configuring the slave Redis instances ### Step 2. Configuring the slave Redis instances
1. SSH into the **slave** Redis server. 1. SSH into the **slave** Redis server.
...@@ -308,11 +312,9 @@ The prerequisites for a HA Redis setup are the following: ...@@ -308,11 +312,9 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents: 1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby ```ruby
# Enable the slave role and disable all other services in the machine # Specify server role as 'redis_slave_role'
# (you can still enable Sentinel). This will also set automatically roles ['redis_slave_role']
# `redis['master'] = false`.
redis_slave_role['enable'] = true
# IP address pointing to a local IP that the other machines can reach to. # IP address pointing to a local IP that the other machines can reach to.
# You can also set bind to '0.0.0.0' which listen in all interfaces. # You can also set bind to '0.0.0.0' which listen in all interfaces.
# If you really need to bind to an external accessible IP, make # If you really need to bind to an external accessible IP, make
...@@ -345,6 +347,10 @@ The prerequisites for a HA Redis setup are the following: ...@@ -345,6 +347,10 @@ The prerequisites for a HA Redis setup are the following:
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect. 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
1. Go through the steps again for all the other slave nodes. 1. Go through the steps again for all the other slave nodes.
> Note: You can specify multiple roles like sentinel and redis as:
> roles ['redis_sentinel_role', 'redis_slave_role']. Read more about high
> availability roles at https://docs.gitlab.com/omnibus/roles/
--- ---
These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
...@@ -392,7 +398,7 @@ multiple machines with the Sentinel daemon. ...@@ -392,7 +398,7 @@ multiple machines with the Sentinel daemon.
be duplicate below): be duplicate below):
```ruby ```ruby
redis_sentinel_role['enable'] = true roles ['redis_sentinel_role']
# Must be the same in every sentinel node # Must be the same in every sentinel node
redis['master_name'] = 'gitlab-redis' redis['master_name'] = 'gitlab-redis'
......
...@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on ...@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on
### Usage in HAML/Rails ### Usage in HAML/Rails
To use a sprite Icon in HAML or Rails we use a specific helper function : To use a sprite Icon in HAML or Rails we use a specific helper function :
`sprite_icon(icon_name, size: nil, css_class: '')` `sprite_icon(icon_name, size: nil, css_class: '')`
**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`). **icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) **size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
**css_class (optional)** If you want to add additional css classes **css_class (optional)** If you want to add additional css classes
**Example** **Example**
`= sprite_icon('issues', size: 72, css_class: 'icon-danger')` `= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
...@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function : ...@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function :
`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` `<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
### Usage in Vue
We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`.
Sample usage :
`<icon
name="retry"
:size="32"
css-classes="top"
/>`
**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
**css-classes (optional)** Additional CSS Classes to add to the svg tag.
### Usage in HTML/JS ### Usage in HTML/JS
Please use the following function inside JS to render an icon : Please use the following function inside JS to render an icon :
`gl.utils.spriteIcon(iconName)` `gl.utils.spriteIcon(iconName)`
## Adding a new icon to the sprite ## Adding a new icon to the sprite
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
# SVG Illustrations # SVG Illustrations
......
# GitLab Geo # GitLab Geo
>**Note:**
GitLab Geo is in **Beta** development. It is considered experimental and
not production-ready. It will undergo significant changes over the next year,
and there is significant chance of data loss. For the latest updates, check the
[meta issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/846).
> **Notes:** > **Notes:**
- GitLab Geo is part of [GitLab Enterprise Edition Premium][ee]. - GitLab Geo is part of [GitLab Enterprise Edition Premium][ee].
- Introduced in GitLab Enterprise Edition 8.9. - Introduced in GitLab Enterprise Edition 8.9.
...@@ -14,6 +8,7 @@ and there is significant chance of data loss. For the latest updates, check the ...@@ -14,6 +8,7 @@ and there is significant chance of data loss. For the latest updates, check the
- You should make sure that all nodes run the same GitLab version. - You should make sure that all nodes run the same GitLab version.
- GitLab Geo requires PostgreSQL 9.6 and Git 2.9 in addition to GitLab's usual - GitLab Geo requires PostgreSQL 9.6 and Git 2.9 in addition to GitLab's usual
[minimum requirements](../install/requirements.md) [minimum requirements](../install/requirements.md)
- Using GitLab Geo in combination with High Availability is considered **Beta**
>**Note:** >**Note:**
GitLab Geo changes significantly from release to release. Upgrades **are** GitLab Geo changes significantly from release to release. Upgrades **are**
...@@ -144,7 +139,7 @@ If you installed GitLab using the Omnibus packages (highly recommended): ...@@ -144,7 +139,7 @@ If you installed GitLab using the Omnibus packages (highly recommended):
1. [Install GitLab Enterprise Edition][install-ee] on the server that will serve 1. [Install GitLab Enterprise Edition][install-ee] on the server that will serve
as the **secondary** Geo node. Do not login or set up anything else in the as the **secondary** Geo node. Do not login or set up anything else in the
secondary node for the moment. secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) to the **primary** Geo Node to unlock GitLab Geo. 1. [Upload the GitLab License](../user/admin_area/license.md) on the **primary** Geo Node to unlock GitLab Geo.
1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology). 1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md) 1. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration.md) to set the primary and secondary nodes. 1. [Configure GitLab](configuration.md) to set the primary and secondary nodes.
...@@ -160,7 +155,7 @@ If you installed GitLab from source: ...@@ -160,7 +155,7 @@ If you installed GitLab from source:
1. [Install GitLab Enterprise Edition][install-ee-source] on the server that 1. [Install GitLab Enterprise Edition][install-ee-source] on the server that
will serve as the **secondary** Geo node. Do not login or set up anything will serve as the **secondary** Geo node. Do not login or set up anything
else in the secondary node for the moment. else in the secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) you purchased for GitLab Enterprise Edition to unlock GitLab Geo. 1. [Upload the GitLab License](../user/admin_area/license.md) on the **primary** Geo Node to unlock GitLab Geo.
1. [Setup the database replication](database_source.md) (`primary (read-write) <-> secondary (read-only)` topology). 1. [Setup the database replication](database_source.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md) 1. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration_source.md) to set the primary and secondary 1. [Configure GitLab](configuration_source.md) to set the primary and secondary
......
...@@ -7,19 +7,7 @@ from source**](configuration_source.md) guide. ...@@ -7,19 +7,7 @@ from source**](configuration_source.md) guide.
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages. Before attempting the steps in this stage, [complete all prior stages][toc].
1. [Install GitLab Enterprise Edition][install-ee] on the server that will serve
as the **secondary** Geo node. Do not login or set up anything else in the
secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) to the **primary** Geo Node to unlock GitLab Geo.
1. [Setup the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md)
1. **Configure GitLab to set the primary and secondary nodes.**
1. Optional: [Configure a secondary LDAP server](../administration/auth/ldap.md) for the secondary.
1. [Follow the after setup steps](after_setup.md).
[install-ee]: https://about.gitlab.com/downloads-ee/ "GitLab Enterprise Edition Omnibus packages downloads page"
This is the final step you need to follow in order to setup a Geo node. This is the final step you need to follow in order to setup a Geo node.
...@@ -70,9 +58,9 @@ sensitive data in the database. Any secondary node must have the ...@@ -70,9 +58,9 @@ sensitive data in the database. Any secondary node must have the
1. Execute the command below to display the current encryption key and copy it: 1. Execute the command below to display the current encryption key and copy it:
``` ```
gitlab-rake geo:db:show_encryption_key gitlab-rake geo:db:show_encryption_key
``` ```
1. SSH into the **secondary** node and login as root: 1. SSH into the **secondary** node and login as root:
...@@ -80,12 +68,12 @@ sensitive data in the database. Any secondary node must have the ...@@ -80,12 +68,12 @@ sensitive data in the database. Any secondary node must have the
sudo -i sudo -i
``` ```
1. Add the following to /etc/gitlab/gitlab.rb, replacing `encryption-key` with the output 1. Add the following to `/etc/gitlab/gitlab.rb`, replacing `encryption-key` with the output
of the previous command: of the previous command:
```ruby ```ruby
gitlab_rails['db_key_base'] = "encryption-key" gitlab_rails['db_key_base'] = 'encryption-key'
``` ```
1. Reconfigure the secondary node for the change to take effect: 1. Reconfigure the secondary node for the change to take effect:
...@@ -101,16 +89,24 @@ running and accessible. ...@@ -101,16 +89,24 @@ running and accessible.
### Step 2. Enabling hashed storage (from GitLab 10.0) ### Step 2. Enabling hashed storage (from GitLab 10.0)
>**Warning**
Hashed storage is in **Beta**. It is considered experimental and not
production-ready. For the latest updates, check
[issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821).
Hashed Storage is not required to run GitLab Geo, but in some edge cases race
conditions can lead to errors and Geo to break. Known issues are renaming a
project multiple times in short succession, deleting a project and recreating
with the same name very quickly.
Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes.
1. Visit the **primary** node's **Admin Area ➔ Settings** 1. Visit the **primary** node's **Admin Area ➔ Settings**
(`/admin/application_settings`) in your browser (`/admin/application_settings`) in your browser
1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`: 1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`:
![](img/hashed-storage.png) ![](img/hashed-storage.png)
Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes - so we recommend it is
used for all GitLab Geo installations.
### Step 3. (Optional) Configuring the secondary to trust the primary ### Step 3. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate. You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
...@@ -221,3 +217,5 @@ See the [updating the Geo nodes document](updating_the_geo_nodes.md). ...@@ -221,3 +217,5 @@ See the [updating the Geo nodes document](updating_the_geo_nodes.md).
## Troubleshooting ## Troubleshooting
See the [troubleshooting document](troubleshooting.md). See the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-omnibus-gitlab
...@@ -7,18 +7,7 @@ using the Omnibus GitLab packages, follow the ...@@ -7,18 +7,7 @@ using the Omnibus GitLab packages, follow the
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages. Before attempting the steps in this stage, [complete all prior stages][toc].
1. [Install GitLab Enterprise Edition][install-ee-source] on the server that
will serve as the **secondary** Geo node. Do not login or set up anything
else in the secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) you purchased for GitLab Enterprise Edition to unlock GitLab Geo.
1. [Setup the database replication](database_source.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md)
1. **Configure GitLab to set the primary and secondary nodes.**
1. [Follow the after setup steps](after_setup.md).
[install-ee-source]: https://docs.gitlab.com/ee/install/installation.html "GitLab Enterprise Edition installation from source"
This is the final step you need to follow in order to setup a Geo node. This is the final step you need to follow in order to setup a Geo node.
...@@ -72,9 +61,9 @@ sensitive data in the database. Any secondary node must have the ...@@ -72,9 +61,9 @@ sensitive data in the database. Any secondary node must have the
1. Execute the command below to display the current encryption key and copy it: 1. Execute the command below to display the current encryption key and copy it:
``` ```
sudo -u git -H bundle exec rake geo:db:show_encryption_key RAILS_ENV=production sudo -u git -H bundle exec rake geo:db:show_encryption_key RAILS_ENV=production
``` ```
1. SSH into the **secondary** node and login as root: 1. SSH into the **secondary** node and login as root:
...@@ -85,9 +74,9 @@ sensitive data in the database. Any secondary node must have the ...@@ -85,9 +74,9 @@ sensitive data in the database. Any secondary node must have the
1. Open the `secrets.yml` file and change the value of `db_key_base` to the 1. Open the `secrets.yml` file and change the value of `db_key_base` to the
output of the previous step: output of the previous step:
``` ```
sudo -u git -H editor config/secrets.yml sudo -u git -H editor config/secrets.yml
``` ```
1. Save and close the file. 1. Save and close the file.
...@@ -104,16 +93,29 @@ immediately. Make sure the secondary instance is running and accessible. ...@@ -104,16 +93,29 @@ immediately. Make sure the secondary instance is running and accessible.
### Step 2. Enabling hashed storage (from GitLab 10.0) ### Step 2. Enabling hashed storage (from GitLab 10.0)
>**Note:**
Hashed storage is in **Beta**. It is considered experimental and not
production-ready. For the latest updates, check
[issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821).
Hashed Storage is not required to run GitLab Geo, but in some edge cases race
conditions can lead to errors and Geo to break. Known issues are renaming a
project multiple times in short succession, deleting a project and recreating
with the same name very quickly.
>**Note:**
Instances already using hashed storage are not recommended to disable hashed
storage, since bugs affecting hashed storage would continue to affect these
projects.
Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes.
1. Visit the **primary** node's **Admin Area ➔ Settings** 1. Visit the **primary** node's **Admin Area ➔ Settings**
(`/admin/application_settings`) in your browser (`/admin/application_settings`) in your browser
1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`: 1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`:
![](img/hashed-storage.png) ![](img/hashed-storage.png)
Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes - so we recommend it is
used for all GitLab Geo installations.
### Step 3. (Optional) Configuring the secondary to trust the primary ### Step 3. (Optional) Configuring the secondary to trust the primary
You can safely skip this step if your primary uses a CA-issued HTTPS certificate. You can safely skip this step if your primary uses a CA-issued HTTPS certificate.
...@@ -187,3 +189,5 @@ Read [Replicating wikis and repositories over SSH](configuration.md#replicating- ...@@ -187,3 +189,5 @@ Read [Replicating wikis and repositories over SSH](configuration.md#replicating-
## Troubleshooting ## Troubleshooting
Read the [troubleshooting document](troubleshooting.md). Read the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-gitlab-installed-from-source
...@@ -7,19 +7,7 @@ from source, follow the ...@@ -7,19 +7,7 @@ from source, follow the
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages. Before attempting the steps in this stage, [complete all prior stages][toc].
1. [Install GitLab Enterprise Edition][install-ee] on the server that will serve
as the **secondary** Geo node. Do not login or set up anything else in the
secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) to the **primary** Geo Node to unlock GitLab Geo.
1. **Setup the database replication** (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration.md) to set the primary and secondary nodes.
1. Optional: [Configure a secondary LDAP server](../administration/auth/ldap.md) for the secondary. See [notes on LDAP](#ldap).
1. [Follow the after setup steps](after_setup.md).
[install-ee]: https://about.gitlab.com/downloads-ee/ "GitLab Enterprise Edition Omnibus packages downloads page"
This document describes the minimal steps you have to take in order to This document describes the minimal steps you have to take in order to
replicate your GitLab database into another server. You may have to change replicate your GitLab database into another server. You may have to change
...@@ -79,17 +67,17 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -79,17 +67,17 @@ will not be able to perform all necessary configuration steps. Refer to
This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`. This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
1. Omnibus GitLab has already a replication user called `gitlab_replicator`. 1. Omnibus GitLab already has a replication user called `gitlab_replicator`.
You must set its password manually. You will be prompted to enter a You must set its password manually. You will be prompted to enter a
password: password:
```bash ```bash
gitlab-ctl set-replication-password gitlab-ctl set-replication-password
``` ```
This command will also read `postgresql['sql_replication_user']` Omnibus This command will also read `postgresql['sql_replication_user']` Omnibus
setting in case you have changed `gitlab_replicator` username to something setting in case you have changed `gitlab_replicator` username to something
else. else.
1. Set up TLS support for the PostgreSQL primary server 1. Set up TLS support for the PostgreSQL primary server
> **Warning**: Only skip this step if you **know** that PostgreSQL traffic > **Warning**: Only skip this step if you **know** that PostgreSQL traffic
...@@ -151,8 +139,8 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -151,8 +139,8 @@ will not be able to perform all necessary configuration steps. Refer to
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
``` ```
1. Add this configuration to `/etc/gitlab/gitlab.rb`. Additional options are 1. Add this configuration to `/etc/gitlab/gitlab.rb`. Additional options are
documented [here](http://docs.gitlab.com/omnibus/settings/database.html#enabling-ssl). documented [here](http://docs.gitlab.com/omnibus/settings/database.html#enabling-ssl).
```ruby ```ruby
postgresql['ssl'] = 'on' postgresql['ssl'] = 'on'
...@@ -165,7 +153,7 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -165,7 +153,7 @@ will not be able to perform all necessary configuration steps. Refer to
```ruby ```ruby
geo_primary_role['enable'] = true geo_primary_role['enable'] = true
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']
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32'] postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32']
# New for 9.4: Set this to be the number of Geo secondary nodes you have # New for 9.4: Set this to be the number of Geo secondary nodes you have
...@@ -214,7 +202,7 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -214,7 +202,7 @@ will not be able to perform all necessary configuration steps. Refer to
```ruby ```ruby
# Example configuration using internal IPs for a cloud configuration # Example configuration using internal IPs for a cloud configuration
geo_primary_role['enable'] = true geo_primary_role['enable'] = true
postgresql['listen_address'] = "10.1.5.3" postgresql['listen_address'] = '10.1.5.3'
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','10.1.5.3/32'] postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','10.1.5.3/32']
postgresql['md5_auth_cidr_addresses'] = ['10.1.10.5/32'] postgresql['md5_auth_cidr_addresses'] = ['10.1.10.5/32']
postgresql['max_replication_slots'] = 1 # Number of Geo secondary nodes postgresql['max_replication_slots'] = 1 # Number of Geo secondary nodes
...@@ -225,7 +213,7 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -225,7 +213,7 @@ will not be able to perform all necessary configuration steps. Refer to
If you prefer that your nodes communicate over the public Internet, you If you prefer that your nodes communicate over the public Internet, you
may choose the IP addresses from the "External IP" column above. may choose the IP addresses from the "External IP" column above.
1. Optional: If you want to add another secondary, the relevant setting would look like: 1. Optional: If you want to add another secondary, the relevant setting would look like:
```ruby ```ruby
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32','11.22.33.44/32'] postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32','11.22.33.44/32']
...@@ -237,16 +225,46 @@ will not be able to perform all necessary configuration steps. Refer to ...@@ -237,16 +225,46 @@ will not be able to perform all necessary configuration steps. Refer to
1. Check to make sure your firewall rules are set so that the secondary nodes 1. Check to make sure your firewall rules are set so that the secondary nodes
can access port `5432` on the primary node. can access port `5432` on the primary node.
1. Save the file and [reconfigure GitLab][] for the DB listen changes to take effect.
This will fail and is expected. 1. Save the file and [reconfigure GitLab][] for the database listen changes to
1. You will need to manually restart postgres `gitlab-ctl restart postgresql` until [Omnibus#2797](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2797) gets fixed. take effect.
1. You should now reconfigure again, and it should complete cleanly.
1. New for 9.4: Restart your primary PostgreSQL server to ensure the replication slot changes **This step will fail.** This is caused by
take effect (`sudo gitlab-ctl restart postgresql` for Omnibus-provided PostgreSQL). [Omnibus#2797](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2797).
Restart PostgreSQL:
```bash
gitlab-ctl restart postgresql
```
[Reconfigure GitLab][reconfigure GitLab] again. It should complete cleanly.
1. New for 9.4: Restart your primary PostgreSQL server to ensure the
replication slot changes take effect (`sudo gitlab-ctl restart postgresql`
for Omnibus-provided PostgreSQL).
1. Now that the PostgreSQL server is set up to accept remote connections, run 1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt` to make sure that PostgreSQL is listening on port `5432` to `netstat -plnt` to make sure that PostgreSQL is listening on port `5432` to
the server's public IP. the server's public IP.
1. Verify that clock synchronization is enabled.
>**Important:**
For Geo to work correctly, all nodes must have their clocks
synchronized. It is not required for all nodes to be set to the same time
zone, but when the respective times are converted to UTC time, the clocks
must be synchronized to within 60 seconds of each other.
If you are using Ubuntu, verify NTP sync is enabled:
```bash
timedatectl status | grep 'NTP synchronized'
```
Refer to your Linux distribution documentation to setup clock
synchronization. This can easily be done using any NTP-compatible daemon.
### Step 2. Add the secondary GitLab node ### Step 2. Add the secondary GitLab node
To prevent the secondary geo node trying to act as the primary once the To prevent the secondary geo node trying to act as the primary once the
...@@ -329,14 +347,22 @@ primary before the database is replicated. ...@@ -329,14 +347,22 @@ primary before the database is replicated.
1. [Reconfigure GitLab][] for the changes to take effect. 1. [Reconfigure GitLab][] for the changes to take effect.
1. Setup clock synchronization service in your Linux distro. 1. Verify that clock synchronization is enabled.
This can easily be done via any NTP-compatible daemon. For example,
here are [instructions for setting up NTP with Ubuntu](https://help.ubuntu.com/lts/serverguide/NTP.html). >**Important:**
For Geo to work correctly, all nodes must have their clocks
synchronized. It is not required for all nodes to be set to the same time
zone, but when the respective times are converted to UTC time, the clocks
must be synchronized to within 60 seconds of each other.
If you are using Ubuntu, verify NTP sync is enabled:
```bash
timedatectl status | grep 'NTP synchronized'
```
**IMPORTANT:** For Geo to work correctly, all nodes must be with their Refer to your Linux distribution documentation to setup clock
clocks synchronized. It is not required for all nodes to be set to the synchronization. This can easily be done using any NTP-compatible daemon.
same time zone, but when the respective times are converted to UTC time,
the clocks must be synchronized to within 60 seconds of each other.
### Step 4. Initiate the replication process ### Step 4. Initiate the replication process
...@@ -357,19 +383,19 @@ data before running `pg_basebackup`. ...@@ -357,19 +383,19 @@ data before running `pg_basebackup`.
sudo -i sudo -i
``` ```
1. New for 9.4: Choose a database-friendly name to use for your secondary to use as the 1. New for 9.4: Choose a database-friendly name to use for your secondary to
replication slot name. For example, if your domain is use as the replication slot name. For example, if your domain is
`geo-secondary.mydomain.com`, you may use `geo_secondary_my_domain_com` as `secondary.geo.example.com`, you may use `secondary_example` as the slot
the slot name. name.
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 # Certificate and key currently used by GitLab
gitlab-ctl replicate-geo-database --host=geo.primary.my.domain.com --slot-name=geo_secondary_my_domain_com gitlab-ctl replicate-geo-database --host=primary.geo.example.com --slot-name=secondary_example
# Self-signed certificate and key # Self-signed certificate and key
gitlab-ctl replicate-geo-database --host=1.2.3.4 --slot-name=geo_secondary_my_domain_com --sslmode=verify-ca gitlab-ctl replicate-geo-database --host=1.2.3.4 --slot-name=secondary_example --sslmode=verify-ca
``` ```
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.
...@@ -472,3 +498,4 @@ Read the [troubleshooting document](troubleshooting.md). ...@@ -472,3 +498,4 @@ Read the [troubleshooting document](troubleshooting.md).
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[external postgresql]: #external-postgresql-instances [external postgresql]: #external-postgresql-instances
[tracking]: database_source.md#enable-tracking-database-on-the-secondary-server [tracking]: database_source.md#enable-tracking-database-on-the-secondary-server
[toc]: README.md#using-omnibus-gitlab
...@@ -7,19 +7,7 @@ using the Omnibus GitLab packages, follow the ...@@ -7,19 +7,7 @@ using the Omnibus GitLab packages, follow the
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages. Before attempting the steps in this stage, [complete all prior stages][toc].
1. [Install GitLab Enterprise Edition][install-ee-source] on the server that
will serve as the **secondary** Geo node. Do not login or set up anything
else in the secondary node for the moment.
1. [Upload the GitLab License](../user/admin_area/license.md) you purchased for GitLab Enterprise Edition to unlock GitLab Geo.
1. **Setup the database replication topology** (`primary (read-write) <-> secondary (read-only)`)
1. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration_source.md) to set the primary and secondary
nodes.
1. [Follow the after setup steps](after_setup.md).
[install-ee-source]: https://docs.gitlab.com/ee/install/installation.html "GitLab Enterprise Edition installation from source"
This document describes the minimal steps you have to take in order to This document describes the minimal steps you have to take in order to
replicate your GitLab database into another server. You may have to change replicate your GitLab database into another server. You may have to change
...@@ -186,34 +174,51 @@ The following guide assumes that: ...@@ -186,34 +174,51 @@ The following guide assumes that:
secondary, add one more row like the replication one and change the IP secondary, add one more row like the replication one and change the IP
address: address:
```bash ```bash
host all all 127.0.0.1/32 trust host all all 127.0.0.1/32 trust
host all all 1.2.3.4/32 trust host all all 1.2.3.4/32 trust
host replication gitlab_replicator 5.6.7.8/32 md5 host replication gitlab_replicator 5.6.7.8/32 md5
host replication gitlab_replicator 11.22.33.44/32 md5 host replication gitlab_replicator 11.22.33.44/32 md5
``` ```
1. Restart PostgreSQL for the changes to take effect. 1. Restart PostgreSQL for the changes to take effect.
1. Choose a database-friendly name to use for your secondary to use as the 1. Choose a database-friendly name to use for your secondary to use as the
replication slot name. For example, if your domain is replication slot name. For example, if your domain is
`geo-secondary.mydomain.com`, you may use `geo_secondary_my_domain_com` as `secondary.geo.example.com`, you may use `secondary_example` as the slot
the slot name. name.
1. Create the replication slot on the primary: 1. Create the replication slot on the primary:
``` ```
$ sudo -u postgres psql -c "SELECT * FROM pg_create_physical_replication_slot('geo_secondary_my_domain');" $ sudo -u postgres psql -c "SELECT * FROM pg_create_physical_replication_slot('secondary_example');"
slot_name | xlog_position slot_name | xlog_position
-------------------------+--------------- ------------------+---------------
geo_secondary_my_domain | secondary_example |
(1 row) (1 row)
``` ```
1. Now that the PostgreSQL server is set up to accept remote connections, run 1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt` to make sure that PostgreSQL is listening to the server's `netstat -plnt` to make sure that PostgreSQL is listening to the server's
public IP. public IP.
1. Verify that clock synchronization is enabled.
>**Important:**
For Geo to work correctly, all nodes must have their clocks
synchronized. It is not required for all nodes to be set to the same time
zone, but when the respective times are converted to UTC time, the clocks
must be synchronized to within 60 seconds of each other.
If you are using Ubuntu, verify NTP sync is enabled:
```bash
timedatectl status | grep 'NTP synchronized'
```
Refer to your Linux distribution documentation to setup clock
synchronization. This can easily be done using any NTP-compatible daemon.
### Step 2. Add the secondary GitLab node ### Step 2. Add the secondary GitLab node
To prevent the secondary geo node trying to act as the primary once the To prevent the secondary geo node trying to act as the primary once the
...@@ -267,15 +272,15 @@ primary before the database is replicated. ...@@ -267,15 +272,15 @@ primary before the database is replicated.
If you're using a CA-issued certificate and connecting by FQDN: If you're using a CA-issued certificate and connecting by FQDN:
``` ```
sudo -u postgres psql -h primary.geo.example.com -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-ca" -W sudo -u postgres psql -h primary.geo.example.com -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-ca" -W
``` ```
If you're using a self-signed certificate or connecting by IP address: If you're using a self-signed certificate or connecting by IP address:
``` ```
sudo -u postgres psql -h 1.2.3.4 -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-full" -W sudo -u postgres psql -h 1.2.3.4 -U gitlab_replicator -d "dbname=gitlabhq_production sslmode=verify-full" -W
``` ```
When prompted enter the password you set in the first step for the When prompted enter the password you set in the first step for the
`gitlab_replicator` user. If all worked correctly, you should see the `gitlab_replicator` user. If all worked correctly, you should see the
...@@ -307,19 +312,27 @@ primary before the database is replicated. ...@@ -307,19 +312,27 @@ primary before the database is replicated.
#### Enable tracking database on the secondary server #### Enable tracking database on the secondary server
Geo secondary nodes use a tracking database to keep track of replication status and recover Geo secondary nodes use a tracking database to keep track of replication status
automatically from some replication issues. and recover automatically from some replication issues.
It is added in GitLab 9.1, and since GitLab 10.0 it is required. It is added in GitLab 9.1, and since GitLab 10.0 it is required.
> **IMPORTANT:** For this feature to work correctly, all nodes must be 1. Verify that clock synchronization is enabled.
with their clocks synchronized. It is not required for all nodes to be set to
the same time zone, but when the respective times are converted to UTC time,
the clocks must be synchronized to within 60 seconds of each other.
1. Setup clock synchronization service in your Linux distro. >**Important:**
This can easily be done via any NTP-compatible daemon. For example, For Geo to work correctly, all nodes must have their clocks
here are [instructions for setting up NTP with Ubuntu](https://help.ubuntu.com/lts/serverguide/NTP.html). synchronized. It is not required for all nodes to be set to the same time
zone, but when the respective times are converted to UTC time, the clocks
must be synchronized to within 60 seconds of each other.
If you are using Ubuntu, verify NTP sync is enabled:
```bash
timedatectl status | grep 'NTP synchronized'
```
Refer to your Linux distribution documentation to setup clock
synchronization. This can easily be done using any NTP-compatible daemon.
1. Create `database_geo.yml` with the information of your secondary PostgreSQL 1. Create `database_geo.yml` with the information of your secondary PostgreSQL
database. Note that GitLab will set up another database instance separate database. Note that GitLab will set up another database instance separate
...@@ -332,19 +345,19 @@ the clocks must be synchronized to within 60 seconds of each other. ...@@ -332,19 +345,19 @@ the clocks must be synchronized to within 60 seconds of each other.
1. Edit the content of `database_geo.yml` in `production:` like the example below: 1. Edit the content of `database_geo.yml` in `production:` like the example below:
```yaml ```yaml
# #
# PRODUCTION # PRODUCTION
# #
production: production:
adapter: postgresql adapter: postgresql
encoding: unicode encoding: unicode
database: gitlabhq_geo_production database: gitlabhq_geo_production
pool: 10 pool: 10
username: gitlab_geo username: gitlab_geo
# password: # password:
host: /var/opt/gitlab/geo-postgresql host: /var/opt/gitlab/geo-postgresql
``` ```
1. Create the database `gitlabhq_geo_production` in that PostgreSQL 1. Create the database `gitlabhq_geo_production` in that PostgreSQL
instance. instance.
...@@ -466,3 +479,4 @@ Read the [troubleshooting document](troubleshooting.md). ...@@ -466,3 +479,4 @@ Read the [troubleshooting document](troubleshooting.md).
[pgback]: http://www.postgresql.org/docs/9.6/static/app-pgbasebackup.html [pgback]: http://www.postgresql.org/docs/9.6/static/app-pgbasebackup.html
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[toc]: README.md#using-gitlab-installed-from-source
# Epics
> Introduced in [GitLab Enterprise Edition Ultimate][ee] 10.2.
Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
milestones.
## Creating an epic
A paginated list of epics is available in each group from where you can create
a new epic. From your group page:
1. Go to **Epics**
1. Click the **New epic** button at the top right
1. Enter a descriptive title and hit **Create epic**
Once created, you will be taken to the view for that newly-created epic where
you can change its title, description, planned start date, and planned end date.
The planned end date cannot be before the planned start date
(but they can be the same day).
![epic view](img/epic_view.png)
An epic contains a list of issues, and an issue can be associated with at most
one epic. You can add issues associated with the epic by clicking the
plus icon (<kbd>+</kbd>) under the epic description, pasting the link of the
issue, and clicking **Add**. Any issue belonging to a project in the epic's
group or any of the epic's subgroups are eligible to be added. To remove an
issue from an epic, simply click on the <kbd>x</kbd> button in the epic's
issue list.
When you add an issue to an epic that's already associated with another epic,
the issue is automatically removed from the previous epic. In other words, an
issue can be associated with at most one epic.
## Deleting an epic
NOTE: **Note:**
To delete an epic, you need to be an [Owner][permissions] of a group/subgroup.
When inside a single epic view, click the **Delete** button to delete the epic.
A modal will pop-up to confirm your action.
Deleting an epic releases all existing issues from their associated epic in the
system.
## Permissions
If you have access to view an epic and have access to view an issue already
added to that epic, then you can view the issue in the epic issue list.
If you have access to edit an epic and have access to edit an issue, then you
can add the issue to or remove it from the epic.
Note that for a given group, the visibility of all projects must be the same as
the group, or less restrictive. That means if you have access to a group's epic,
then you already have access to its projects' issues.
You may also consult the [group permissions table][permissions].
[ee]: https://about.gitlab.com/gitlab-ee/
[permissions]: ../../permissions.md#group-members-permissions
...@@ -61,39 +61,6 @@ Issues and merge requests are part of projects. For a given group, view all the ...@@ -61,39 +61,6 @@ Issues and merge requests are part of projects. For a given group, view all the
[issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group, [issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
together in a single list view. together in a single list view.
## Epics
Epics are available in GitLab Enterprise Edition Ultimate. Epics are designed to
enable you plan and track work at the feature level, as opposed to the design and
implementation details level of an issue.
Epics are scoped at the group level. A paginated list of epics is available in each
group. From this page, you can also click the `New epic` button at the top right to
create a new epic. Creating a new epic will bring you to the epic view itself for
that newly created epic.
![epics list view](img/epics-list-view.png)
When creating an epic, you only specify its title.
For existing epics, you can add/change its title, description, planned start date, and planned
end date. The planned end date cannot be before the planned start date (but they
can be the same day).
An epic contains a list of issues. And an issue can be associated with at most one epic.
In an epic, you add (and remove) issues associated with the epic by clicking `+`,
pasting in the link to the issue, and clicking `Add`. Any issue belonging to a project
in the epic's group or any of the epic's subgroups can are eligible to be added.
When you add an issue to an epic that's already associated with another epic,
the issue is automatically removed from the previous epic. In other words, an issue
can be associated with at most one epic.
You can also delete an epic from the epic view. Deleting an epic releases all existing
issues fromt their associated epic in the system.
![epic view](img/epic-view.png)
[See group permissions for epics and associating issues.](../permissions.md#group-members-permissions)
## Create a new group ## Create a new group
> **Notes:** > **Notes:**
...@@ -208,6 +175,16 @@ Alternatively, you can [lock the sharing with group feature](#share-with-group-l ...@@ -208,6 +175,16 @@ Alternatively, you can [lock the sharing with group feature](#share-with-group-l
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for more information. See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for more information.
## Epics
> Introduced in [GitLab Enterprise Edition Ultimate][ee] 10.2.
Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
milestones.
[Learn more about Epics.](epics/index.md)
## Group settings ## Group settings
Once you have created a group, you can manage its settings by navigating to Once you have created a group, you can manage its settings by navigating to
......
...@@ -151,16 +151,7 @@ group. ...@@ -151,16 +151,7 @@ group.
| View internal group epic | ✓ | ✓ | ✓ | ✓ | ✓ | | View internal group epic | ✓ | ✓ | ✓ | ✓ | ✓ |
| View public group epic | ✓ | ✓ | ✓ | ✓ | ✓ | | View public group epic | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit group epic | | ✓ | ✓ | ✓ | ✓ | | Create/edit group epic | | ✓ | ✓ | ✓ | ✓ |
| Delete group epic | | | | | ✓ |
If you have access to view an epic and have access to view an issue already added to that epic,
then you can view the issue in the epic issue list.
If you have access to edit an epic and have access to edit an issue, then you can
add the issue to or remove it from the epic.
Note that for a given group, the visibility of all projects must be the same as the
group, or less restrictive. So that means if you have access to a group's epic, then
you already have access to its projects' issues.
### Subgroup permissions ### Subgroup permissions
......
...@@ -122,6 +122,7 @@ ...@@ -122,6 +122,7 @@
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-inline-edit-button="true" :show-inline-edit-button="true"
:show-delete-button="false" :show-delete-button="false"
:can-attach-file="false"
/> />
</div> </div>
<epic-sidebar <epic-sidebar
......
module EE
module Identity
extend ActiveSupport::Concern
prepended do
validates :secondary_extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
scope :with_secondary_extern_uid, ->(provider, secondary_extern_uid) do
iwhere(secondary_extern_uid: normalize_uid(provider, secondary_extern_uid)).with_provider(provider)
end
end
end
end
module EE
# LFS Object EE mixin
#
# This module is intended to encapsulate EE-specific model logic
# and be prepended in the `LfsObject` model
module LfsObject
extend ActiveSupport::Concern
prepended do
after_destroy :log_geo_event
end
def local_store?
[nil, LfsObjectUploader::LOCAL_STORE].include?(self.file_store)
end
private
def log_geo_event
::Geo::LfsObjectDeletedEventStore.new(self).create
end
end
end
...@@ -15,7 +15,6 @@ class License < ActiveRecord::Base ...@@ -15,7 +15,6 @@ class License < ActiveRecord::Base
group_webhooks group_webhooks
issuable_default_templates issuable_default_templates
issue_board_focus_mode issue_board_focus_mode
issue_board_milestone
issue_weights issue_weights
jenkins_integration jenkins_integration
ldap_group_sync ldap_group_sync
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
.panel-heading .panel-heading
Details Details
%ul.well-list %ul.well-list
%li
%span.light Plan:
%strong= @license.plan.capitalize
%li %li
%span.light Uploaded: %span.light Uploaded:
%strong= time_ago_with_tooltip @license.created_at %strong= time_ago_with_tooltip @license.created_at
...@@ -77,6 +80,7 @@ ...@@ -77,6 +80,7 @@
%tr %tr
- @license.licensee.keys.each do |label| - @license.licensee.keys.each do |label|
%th= label %th= label
%th Plan
%th Uploaded at %th Uploaded at
%th Started at %th Started at
%th Expired at %th Expired at
...@@ -86,6 +90,9 @@ ...@@ -86,6 +90,9 @@
%tr %tr
- @license.licensee.keys.each do |label| - @license.licensee.keys.each do |label|
%td= license.licensee[label] %td= license.licensee[label]
%td
%span
= license.plan.capitalize
%td %td
%span %span
= license.created_at = license.created_at
......
...@@ -108,7 +108,7 @@ module EE ...@@ -108,7 +108,7 @@ module EE
end end
def member_uid_to_dn(uid) def member_uid_to_dn(uid)
identity = Identity.find_by(provider: provider, secondary_extern_uid: uid) identity = ::Identity.with_secondary_extern_uid(provider, uid).take
if identity.present? if identity.present?
# Use the DN on record in GitLab when it's available # Use the DN on record in GitLab when it's available
...@@ -127,8 +127,7 @@ module EE ...@@ -127,8 +127,7 @@ module EE
end end
def update_identity(dn, uid) def update_identity(dn, uid)
identity = identity = ::Identity.with_extern_uid(provider, dn).take
Identity.find_by(provider: provider, extern_uid: dn)
# User may not exist in GitLab yet. Skip. # User may not exist in GitLab yet. Skip.
return unless identity.present? return unless identity.present?
......
...@@ -41,18 +41,14 @@ module Gitlab ...@@ -41,18 +41,14 @@ module Gitlab
batch.each do |event_log| batch.each do |event_log|
next unless can_replay?(event_log) next unless can_replay?(event_log)
if event_log.repository_updated_event begin
handle_repository_updated(event_log) event = event_log.event
elsif event_log.repository_created_event handler = "handle_#{event.class.name.demodulize.underscore}"
handle_repository_created(event_log)
elsif event_log.repository_deleted_event __send__(handler, event, event_log.created_at) # rubocop:disable GitlabSecurity/PublicSend
handle_repository_deleted(event_log) rescue NoMethodError => e
elsif event_log.repositories_changed_event logger.error(e.message)
handle_repositories_changed(event_log.repositories_changed_event) raise e
elsif event_log.repository_renamed_event
handle_repository_renamed(event_log)
elsif event_log.hashed_storage_migrated_event
handle_hashed_storage_migrated(event_log)
end end
end end
end end
...@@ -85,12 +81,11 @@ module Gitlab ...@@ -85,12 +81,11 @@ module Gitlab
Gitlab::Geo.current_node&.projects_include?(event_log.project_id) Gitlab::Geo.current_node&.projects_include?(event_log.project_id)
end end
def handle_repository_created(event_log) def handle_repository_created_event(event, created_at)
event = event_log.repository_created_event
registry = find_or_initialize_registry(event.project_id, resync_repository: true, resync_wiki: event.wiki_path.present?) registry = find_or_initialize_registry(event.project_id, resync_repository: true, resync_wiki: event.wiki_path.present?)
logger.event_info( logger.event_info(
event_log.created_at, created_at,
message: 'Repository created', message: 'Repository created',
project_id: event.project_id, project_id: event.project_id,
repo_path: event.repo_path, repo_path: event.repo_path,
...@@ -103,12 +98,11 @@ module Gitlab ...@@ -103,12 +98,11 @@ module Gitlab
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now) ::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end end
def handle_repository_updated(event_log) def handle_repository_updated_event(event, created_at)
event = event_log.repository_updated_event
registry = find_or_initialize_registry(event.project_id, "resync_#{event.source}" => true) registry = find_or_initialize_registry(event.project_id, "resync_#{event.source}" => true)
logger.event_info( logger.event_info(
event_log.created_at, created_at,
message: 'Repository update', message: 'Repository update',
project_id: event.project_id, project_id: event.project_id,
source: event.source, source: event.source,
...@@ -120,15 +114,13 @@ module Gitlab ...@@ -120,15 +114,13 @@ module Gitlab
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now) ::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end end
def handle_repository_deleted(event_log) def handle_repository_deleted_event(event, created_at)
event = event_log.repository_deleted_event
job_id = ::Geo::RepositoryDestroyService job_id = ::Geo::RepositoryDestroyService
.new(event.project_id, event.deleted_project_name, event.deleted_path, event.repository_storage_name) .new(event.project_id, event.deleted_project_name, event.deleted_path, event.repository_storage_name)
.async_execute .async_execute
logger.event_info( logger.event_info(
event_log.created_at, created_at,
message: 'Deleted project', message: 'Deleted project',
project_id: event.project_id, project_id: event.project_id,
repository_storage_name: event.repository_storage_name, repository_storage_name: event.repository_storage_name,
...@@ -139,7 +131,7 @@ module Gitlab ...@@ -139,7 +131,7 @@ module Gitlab
::Geo::ProjectRegistry.where(project_id: event.project_id).delete_all ::Geo::ProjectRegistry.where(project_id: event.project_id).delete_all
end end
def handle_repositories_changed(event) def handle_repositories_changed_event(event, created_at)
return unless Gitlab::Geo.current_node.id == event.geo_node_id return unless Gitlab::Geo.current_node.id == event.geo_node_id
job_id = ::Geo::RepositoriesCleanUpWorker.perform_in(1.hour, event.geo_node_id) job_id = ::Geo::RepositoriesCleanUpWorker.perform_in(1.hour, event.geo_node_id)
...@@ -151,8 +143,7 @@ module Gitlab ...@@ -151,8 +143,7 @@ module Gitlab
end end
end end
def handle_repository_renamed(event_log) def handle_repository_renamed_event(event, created_at)
event = event_log.repository_renamed_event
return unless event.project_id return unless event.project_id
old_path = event.old_path_with_namespace old_path = event.old_path_with_namespace
...@@ -163,7 +154,7 @@ module Gitlab ...@@ -163,7 +154,7 @@ module Gitlab
.async_execute .async_execute
logger.event_info( logger.event_info(
event_log.created_at, created_at,
message: 'Renaming project', message: 'Renaming project',
project_id: event.project_id, project_id: event.project_id,
old_path: old_path, old_path: old_path,
...@@ -171,8 +162,7 @@ module Gitlab ...@@ -171,8 +162,7 @@ module Gitlab
job_id: job_id) job_id: job_id)
end end
def handle_hashed_storage_migrated(event_log) def handle_hashed_storage_migrated_event(event, created_at)
event = event_log.hashed_storage_migrated_event
return unless event.project_id return unless event.project_id
job_id = ::Geo::HashedStorageMigrationService.new( job_id = ::Geo::HashedStorageMigrationService.new(
...@@ -183,7 +173,7 @@ module Gitlab ...@@ -183,7 +173,7 @@ module Gitlab
).async_execute ).async_execute
logger.event_info( logger.event_info(
event_log.created_at, created_at,
message: 'Migrating project to hashed storage', message: 'Migrating project to hashed storage',
project_id: event.project_id, project_id: event.project_id,
old_storage_version: event.old_storage_version, old_storage_version: event.old_storage_version,
...@@ -193,6 +183,22 @@ module Gitlab ...@@ -193,6 +183,22 @@ module Gitlab
job_id: job_id) job_id: job_id)
end end
def handle_lfs_object_deleted_event(event, created_at)
file_path = File.join(LfsObjectUploader.local_store_path, event.file_path)
job_id = ::Geo::FileRemovalWorker.perform_async(file_path)
logger.event_info(
created_at,
message: 'Deleted LFS object',
oid: event.oid,
file_id: event.lfs_object_id,
file_path: file_path,
job_id: job_id)
::Geo::FileRegistry.lfs_objects.where(file_id: event.lfs_object_id).delete_all
end
def find_or_initialize_registry(project_id, attrs) def find_or_initialize_registry(project_id, attrs)
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id) registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
registry.assign_attributes(attrs) registry.assign_attributes(attrs)
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class MergeRequestParser class MergeRequestParser
FORKED_PROJECT_ID = -1 FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash) def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project @project = project
......
...@@ -44,7 +44,7 @@ module Gitlab ...@@ -44,7 +44,7 @@ module Gitlab
private private
def find_by_login(login) def find_by_login(login)
identity = ::Identity.find_by(provider: :kerberos, extern_uid: login) identity = ::Identity.with_extern_uid(:kerberos, login).take
identity && identity.user identity && identity.user
end end
end end
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
module Gitlab module Gitlab
module ShellAdapter module ShellAdapter
def gitlab_shell def gitlab_shell
Gitlab::Shell.new @gitlab_shell ||= Gitlab::Shell.new
end end
end end
end end
...@@ -8,6 +8,13 @@ ENV DEBIAN_FRONTEND noninteractive ...@@ -8,6 +8,13 @@ ENV DEBIAN_FRONTEND noninteractive
RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
RUN apt-get update && apt-get install -y wget git unzip xvfb RUN apt-get update && apt-get install -y wget git unzip xvfb
##
# Install Docker
#
RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
rm docker-17.09.0-ce.tgz
## ##
# Install Google Chrome version with headless support # Install Google Chrome version with headless support
# #
......
...@@ -49,6 +49,10 @@ module QA ...@@ -49,6 +49,10 @@ module QA
module Sandbox module Sandbox
autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare' autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
end end
module Admin
autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage'
end
end end
end end
...@@ -64,9 +68,11 @@ module QA ...@@ -64,9 +68,11 @@ module QA
autoload :Entry, 'qa/page/main/entry' autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login' autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu' autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
end end
module Dashboard module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups' autoload :Groups, 'qa/page/dashboard/groups'
end end
...@@ -82,6 +88,7 @@ module QA ...@@ -82,6 +88,7 @@ module QA
module Admin module Admin
autoload :Menu, 'qa/page/admin/menu' autoload :Menu, 'qa/page/admin/menu'
autoload :Settings, 'qa/page/admin/settings'
end end
module Mattermost module Mattermost
...@@ -97,6 +104,13 @@ module QA ...@@ -97,6 +104,13 @@ module QA
autoload :Repository, 'qa/git/repository' autoload :Repository, 'qa/git/repository'
end end
##
# Classes describing shell interaction with GitLab
#
module Shell
autoload :Omnibus, 'qa/shell/omnibus'
end
## ##
# Classes that make it possible to execute features tests. # Classes that make it possible to execute features tests.
# #
......
...@@ -6,10 +6,19 @@ module QA ...@@ -6,10 +6,19 @@ module QA
module Page module Page
module Admin module Admin
autoload :License, 'qa/ee/page/admin/license' autoload :License, 'qa/ee/page/admin/license'
autoload :GeoNodes, 'qa/ee/page/admin/geo_nodes'
end end
end end
module Scenario module Scenario
module Geo
autoload :Node, 'qa/ee/scenario/geo/node'
end
module Test
autoload :Geo, 'qa/ee/scenario/test/geo'
end
module License module License
autoload :Add, 'qa/ee/scenario/license/add' autoload :Add, 'qa/ee/scenario/license/add'
end end
......
module QA
module EE
module Page
module Admin
class GeoNodes < QA::Page::Base
def set_node_address(address)
fill_in 'URL', with: address
end
def add_node!
click_button 'Add Node'
end
end
end
end
end
end
module QA
module EE
module Scenario
module Geo
class Node < QA::Scenario::Template
attr_accessor :address
def perform
QA::Page::Main::Entry.act { visit_login_page }
QA::Page::Main::Login.act { sign_in_using_credentials }
QA::Page::Main::Menu.act { go_to_admin_area }
QA::Page::Admin::Menu.act { go_to_geo_nodes }
EE::Page::Admin::GeoNodes.perform do |page|
raise ArgumentError if @address.nil?
page.set_node_address(@address)
page.add_node!
end
QA::Page::Main::Menu.act { sign_out }
end
end
end
end
end
end
module QA
module EE
module Scenario
module Test
class Geo < QA::Scenario::Template
include QA::Scenario::Bootable
attribute :geo_primary_address, '--primary-address PRIMARY'
attribute :geo_primary_name, '--primary-name PRIMARY_NAME'
attribute :geo_secondary_address, '--secondary-address SECONDARY'
attribute :geo_secondary_name, '--secondary-name SECONDARY_NAME'
attribute :geo_skip_setup?, '--without-setup'
def perform(**args)
QA::Specs::Config.act { configure_capybara! }
unless args[:geo_skip_setup?]
# TODO, Factory::License -> gitlab-org/gitlab-qa#86
#
QA::Runtime::Scenario.define(:gitlab_address, args[:geo_primary_address])
Geo::Primary.act do
add_license
enable_hashed_storage
set_replication_password
set_primary_node
add_secondary_node
end
Geo::Secondary.act { replicate_database }
end
Specs::Runner.perform do |specs|
specs.tty = true
specs.tags = %w[geo]
end
end
class Primary
include QA::Scenario::Actable
def initialize
@address = QA::Runtime::Scenario.geo_primary_address
@name = QA::Runtime::Scenario.geo_primary_name
end
def add_license
# TODO EE license to Runtime.license, gitlab-org/gitlab-qa#86
#
puts 'Adding GitLab EE license ...'
Scenario::License::Add.perform(ENV['EE_LICENSE'])
end
def enable_hashed_storage
# TODO, Factory::HashedStorage - gitlab-org/gitlab-qa#86
#
puts 'Enabling hashed repository storage setting ...'
QA::Scenario::Gitlab::Admin::HashedStorage.perform(:enabled)
end
def add_secondary_node
# TODO, Factory::Geo::Node - gitlab-org/gitlab-qa#86
#
puts 'Adding new Geo secondary node ...'
Scenario::Geo::Node.perform do |node|
node.address = QA::Runtime::Scenario.geo_secondary_address
end
end
def set_replication_password
puts 'Setting replication password on primary node ...'
QA::Shell::Omnibus.new(@name).act do
gitlab_ctl 'set-replication-password', input: 'echo mypass'
end
end
def set_primary_node
puts 'Making this node a primary node ...'
Shell::Omnibus.new(@name).act do
gitlab_ctl 'set-geo-primary-node'
end
end
end
class Secondary
include QA::Scenario::Actable
def initialize
@name = QA::Runtime::Scenario.geo_secondary_name
end
def replicate_database
puts 'Starting Geo replication on secondary node ...'
Shell::Omnibus.new(@name).act do
require 'uri'
host = URI(QA::Runtime::Scenario.geo_primary_address).host
slot = QA::Runtime::Scenario.geo_primary_name.tr('-', '_')
gitlab_ctl "replicate-geo-database --host=#{host} --slot-name=#{slot} " \
"--sslmode=disable --no-wait --force", input: 'echo mypass'
end
puts 'Waiting until secondary node services are restarted ...'
sleep 60 # Wait until services are restarted correctly
end
end
end
end
end
end
end
...@@ -7,6 +7,11 @@ module QA ...@@ -7,6 +7,11 @@ module QA
require 'qa/ee' require 'qa/ee'
end end
##
# TODO generic solution for screenshot in factories
#
# gitlab-org/gitlab-qa#86
#
def perform_before_hooks def perform_before_hooks
return unless ENV['EE_LICENSE'] return unless ENV['EE_LICENSE']
......
...@@ -2,9 +2,16 @@ module QA ...@@ -2,9 +2,16 @@ module QA
module Page module Page
module Admin module Admin
class Menu < Page::Base class Menu < Page::Base
def go_to_geo_nodes
click_link 'Geo Nodes'
end
def go_to_license def go_to_license
link = find_link 'License' click_link 'License'
link.click end
def go_to_settings
click_link 'Settings'
end end
end end
end end
......
module QA
module Page
module Admin
class Settings < Page::Base
def enable_hashed_storage
scroll_to 'legend', text: 'Repository Storage'
check 'Create new projects using hashed storage paths'
end
def save_settings
scroll_to '.form-actions' do
click_button 'Save'
end
end
end
end
end
end
require 'capybara/dsl'
module QA module QA
module Page module Page
class Base class Base
...@@ -7,6 +9,21 @@ module QA ...@@ -7,6 +9,21 @@ module QA
def refresh def refresh
visit current_url visit current_url
end end
def scroll_to(selector, text: nil)
page.execute_script <<~JS
var elements = Array.from(document.querySelectorAll('#{selector}'));
var text = '#{text}';
if (text.length > 0) {
elements.find(e => e.textContent === text).scrollIntoView();
} else {
elements[0].scrollIntoView();
}
JS
page.within(selector) { yield } if block_given?
end
end end
end end
end end
module QA
module Page
module Dashboard
class Projects < Page::Base
def go_to_project(name)
find_link(text: name).click
end
end
end
end
end
...@@ -14,7 +14,7 @@ module QA ...@@ -14,7 +14,7 @@ module QA
# #
start = Time.now start = Time.now
while Time.now - start < 240 while Time.now - start < 1000
break if page.has_css?('.application', wait: 10) break if page.has_css?('.application', wait: 10)
refresh refresh
......
...@@ -7,7 +7,10 @@ module QA ...@@ -7,7 +7,10 @@ module QA
end end
def go_to_projects def go_to_projects
within_top_menu { click_link 'Projects' } within_top_menu do
click_link 'Projects'
click_link 'Your projects'
end
end end
def go_to_admin_area def go_to_admin_area
......
module QA
module Page
module Main
class OAuth < Page::Base
def needs_authorization?
page.current_url.include?('/oauth')
end
def authorize!
click_button 'Authorize'
end
end
end
end
end
...@@ -14,6 +14,10 @@ module QA ...@@ -14,6 +14,10 @@ module QA
find('#project_clone').value find('#project_clone').value
end end
def project_name
find('.project-title').text
end
def wait_for_push def wait_for_push
sleep 5 sleep 5
end end
......
...@@ -28,7 +28,7 @@ module QA ...@@ -28,7 +28,7 @@ module QA
private private
def attribute(name, arg, desc) def attribute(name, arg, desc = '')
options.push(Option.new(name, arg, desc)) options.push(Option.new(name, arg, desc))
end end
......
module QA
module Scenario
module Gitlab
module Admin
class HashedStorage < Scenario::Template
def perform(*traits)
raise ArgumentError unless traits.include?(:enabled)
Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_admin_area }
Page::Admin::Menu.act { go_to_settings }
Page::Admin::Settings.act do
enable_hashed_storage
save_settings
end
QA::Page::Main::Menu.act { sign_out }
end
end
end
end
end
end
require 'open3'
module QA
module Shell
class Omnibus
include Scenario::Actable
def initialize(container)
@name = container
end
def gitlab_ctl(command, input: nil)
if input.nil?
shell "docker exec #{@name} gitlab-ctl #{command}"
else
shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
end
end
private
##
# TODO, make it possible to use generic QA framework classes
# as a library - gitlab-org/gitlab-qa#94
#
def shell(command)
puts "Executing `#{command}`"
Open3.popen2e(command) do |_in, out, wait|
out.each { |line| puts line }
if wait.value.exited? && wait.value.exitstatus.nonzero?
raise "Docker command `#{command}` failed!"
end
end
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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