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
rake
grape_logging (1.7.0)
grape
grpc (1.6.6)
grpc (1.7.2)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1)
googleauth (>= 0.5.1, < 0.7)
gssapi (1.2.0)
ffi (>= 1.0.1)
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 */
/* global EditBlob */
/* global NewCommitForm */
import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
......@@ -15,7 +16,7 @@ import './models/project';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
import './services/board_service';
import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
......@@ -84,11 +85,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
......@@ -120,6 +126,46 @@ $(() => {
methods: {
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 eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
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: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
......@@ -58,12 +43,31 @@ export default {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
eventHub.$emit('clearDetailIssue');
} else {
Store.detail.issue = this.issue;
eventHub.$emit('newDetailIssue', this.issue);
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 */
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
......@@ -5,12 +5,13 @@
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
......@@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
},
components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle,
assignees: Assignees,
subscriptions,
},
});
......@@ -18,6 +18,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
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.project_id = obj.project_id;
this.weight = obj.weight;
......@@ -81,6 +86,14 @@ class ListIssue {
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) {
const data = {
issue: {
......
......@@ -2,7 +2,7 @@
import Vue from 'vue';
class BoardService {
export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
......@@ -117,6 +117,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
}
}
window.BoardService = BoardService;
......@@ -2,11 +2,11 @@
import { s__ } from './locale';
import projectSelect from './project_select';
import IssuableIndex from './issuable_index';
/* global Milestone */
import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global NewBranchForm */
import NewBranchForm from './new_branch_form';
/* global NotificationsForm */
/* global NotificationsDropdown */
import groupAvatar from './group_avatar';
......@@ -18,8 +18,7 @@ import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
import NewCommitForm from './new_commit_form';
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
......
......@@ -16,7 +16,6 @@ export default () => {
new LabelsSelect();
new WeightSelect();
new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new DueDateSelectors();
window.sidebar = new Sidebar();
};
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global SubscriptionSelect */
/* global WeightSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
......@@ -12,6 +11,6 @@ export default () => {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
new SubscriptionSelect();
subscriptionSelect();
new WeightSelect();
};
/* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import issueStatusSelect from './issue_status_select';
import './subscription_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
......@@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
new SubscriptionSelect();
subscriptionSelect();
}
setupBulkUpdateActions() {
......
......@@ -34,6 +34,11 @@ export default {
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
......@@ -234,6 +239,7 @@ export default {
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
/>
<div v-else>
<title-component
......
......@@ -17,6 +17,11 @@
type: String,
required: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
components: {
markdownField,
......@@ -36,7 +41,8 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath">
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
......
......@@ -41,6 +41,11 @@
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
components: {
lockedWarning,
......@@ -83,7 +88,8 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" />
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy"
......
......@@ -61,11 +61,7 @@ import './line_highlighter';
import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
import './milestone';
import './milestone_select';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
import './notes';
import './notifications_dropdown';
import './notifications_form';
......@@ -81,9 +77,6 @@ import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
import './subscription';
import './subscription_select';
import initBreadcrumbs from './breadcrumb';
// 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 */
import Flash from './flash';
(function() {
this.Milestone = (function() {
function Milestone() {
export default class Milestone {
constructor() {
this.bindTabsSwitching();
// Load merge request tab if it is active
......@@ -15,24 +13,24 @@ import Flash from './flash';
this.loadInitialTab();
}
Milestone.prototype.bindTabsSwitching = function() {
bindTabsSwitching() {
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
location.hash = $target.attr('href');
this.loadTab($target);
});
};
Milestone.prototype.loadInitialTab = function() {
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
}
// eslint-disable-next-line class-methods-use-this
loadTab($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
......@@ -47,8 +45,5 @@ import Flash from './flash';
$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 */
import RefSelectDropdown from '~/ref_select_dropdown';
import RefSelectDropdown from './ref_select_dropdown';
(function() {
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
export default class NewBranchForm {
constructor(form, availableRefs) {
this.validate = this.validate.bind(this);
this.branchNameError = form.find('.js-branch-name-error');
this.name = form.find('.js-branch-name');
......@@ -14,17 +13,17 @@ import RefSelectDropdown from '~/ref_select_dropdown';
this.init();
}
NewBranchForm.prototype.addBinding = function() {
addBinding() {
return this.name.on('blur', this.validate);
};
}
NewBranchForm.prototype.init = function() {
init() {
if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
};
}
NewBranchForm.prototype.setupRestrictions = function() {
setupRestrictions() {
var endsWith, invalid, single, startsWith;
startsWith = {
pattern: /^(\/|\.)/g,
......@@ -47,9 +46,9 @@ import RefSelectDropdown from '~/ref_select_dropdown';
conjunction: "or"
};
return this.restrictions = [startsWith, invalid, endsWith, single];
};
}
NewBranchForm.prototype.validate = function() {
validate() {
var errorMessage, errors, formatter, unique, validator;
const indexOf = [].indexOf;
......@@ -90,8 +89,5 @@ import RefSelectDropdown from '~/ref_select_dropdown';
errorMessage = $("<span/>").text(errors.join(', '));
return this.branchNameError.append(errorMessage);
}
};
return NewBranchForm;
})();
}).call(window);
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
this.NewCommitForm = (function() {
function NewCommitForm(form) {
export default class NewCommitForm {
constructor(form) {
this.form = form;
this.renderDestination = this.renderDestination.bind(this);
this.branchName = form.find('.js-branch-name');
......@@ -12,7 +11,7 @@
this.renderDestination();
}
NewCommitForm.prototype.renderDestination = function() {
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
if (different) {
......@@ -25,8 +24,5 @@
this.createMergeRequest.prop('checked', false);
}
return this.wasDifferent = different;
};
return NewCommitForm;
})();
}).call(window);
}
}
......@@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
export default {
......@@ -21,7 +22,7 @@ export default {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
Flash('Error occurred when toggling the notification subscription');
Flash(__('Error occurred when toggling the notification subscription'));
});
},
},
......
......@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: false,
},
id: {
type: Number,
required: false,
},
},
components: {
loadingButton,
......@@ -32,7 +36,7 @@ export default {
},
methods: {
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 {
constructor() {
$('.js-subscription-event').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
return $(el).glDropdown({
return $(element).glDropdown({
selectable: true,
fieldName: fieldName,
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Subscription';
$item = instance.dropdown.find('.is-active');
fieldName,
toggleLabel(selected, el, instance) {
let label = 'Subscription';
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
},
clicked(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
}
id(obj, el) {
return $(el).data('id');
},
});
});
}
}
window.SubscriptionSelect = SubscriptionSelect;
......@@ -6,10 +6,9 @@
Sample configuration:
<icon
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
name="retry"
:size="32"
css-classes="top"
/>
*/
......
......@@ -25,6 +25,11 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -129,6 +134,7 @@
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/>
</div>
</div>
......
......@@ -9,6 +9,11 @@
type: String,
required: false,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
......@@ -41,7 +46,10 @@
are supported
</template>
</div>
<span class="uploading-container">
<span
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
......
......@@ -86,6 +86,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
......@@ -45,7 +45,7 @@ module KerberosSpnegoHelper
krb_principal = spnego_credentials!(spnego_token)
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
end
......
......@@ -26,6 +26,10 @@ module Geo
class_name: 'Geo::HashedStorageMigratedEvent',
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
order(id: :desc).first
end
......@@ -36,7 +40,8 @@ module Geo
repository_deleted_event ||
repository_renamed_event ||
repositories_changed_event ||
hashed_storage_migrated_event
hashed_storage_migrated_event ||
lfs_object_deleted_event
end
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
scope :dirty, -> { where(arel_table[:resync_repository].eq(true).or(arel_table[:resync_wiki].eq(true))) }
def self.failed
repository_sync_failed = arel_table[:last_repository_synced_at].not_eq(nil)
.and(arel_table[:last_repository_successful_sync_at].eq(nil))
wiki_sync_failed = arel_table[:last_wiki_synced_at].not_eq(nil)
.and(arel_table[:last_wiki_successful_sync_at].eq(nil))
repository_sync_failed = arel_table[:repository_retry_count].gt(0)
wiki_sync_failed = arel_table[:wiki_retry_count].gt(0)
where(repository_sync_failed.or(wiki_sync_failed))
end
......
class Identity < ActiveRecord::Base
prepend EE::Identity
include Sortable
include CaseSensitivity
......
......@@ -285,7 +285,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
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)
json[:labels] = labels.as_json(
......
class LfsObject < ActiveRecord::Base
prepend EE::LfsObject
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
......
......@@ -8,6 +8,7 @@ module Geo
class BaseSyncService
include ExclusiveLeaseGuard
include ::Gitlab::Geo::ProjectLogHelpers
include ::Gitlab::ShellAdapter
include Delay
class << self
......@@ -136,7 +137,7 @@ module Geo
if started_at
attrs["last_#{type}_synced_at"] = started_at
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
if finished_at
......@@ -173,13 +174,17 @@ module Geo
registry.public_send("last_#{type}_synced_at") # rubocop:disable GitlabSecurity/PublicSend
end
def disk_path_temp
unless @disk_path_temp
def random_disk_path(prefix)
random_string = SecureRandom.hex(7)
@disk_path_temp = "#{repository.disk_path}_#{random_string}"
"#{repository.disk_path}_#{prefix}#{random_string}"
end
@disk_path_temp
def disk_path_temp
@disk_path_temp ||= random_disk_path('')
end
def deleted_disk_path_temp
@deleted_path ||= "#{repository.disk_path}+failed-geo-sync"
end
def build_temporary_repository
......@@ -199,16 +204,35 @@ module Geo
"Setting newly downloaded repository as main",
storage_path: project.repository_storage_path,
temp_path: disk_path_temp,
deleted_disk_path_temp: deleted_disk_path_temp,
disk_path: repository.disk_path
)
unless gitlab_shell.remove_repository(project.repository_storage_path, repository.disk_path)
raise Gitlab::Shell::Error, 'Can not remove outdated main repository to replace it'
# Remove the deleted path in case it exists, but it may not be there
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
unless gitlab_shell.mv_repository(project.repository_storage_path, disk_path_temp, repository.disk_path)
raise Gitlab::Shell::Error, 'Can not move temporary repository'
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
......@@ -34,9 +34,12 @@ module Geo
return unless Gitlab::Geo.primary?
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
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
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
class RepositorySyncService < BaseSyncService
include Gitlab::ShellAdapter
self.type = :repository
private
......@@ -32,10 +30,12 @@ module Geo
Gitlab::Git::RepositoryMirroring::RemoteError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing repository', e)
registry.increment!(:repository_retry_count)
rescue Gitlab::Git::Repository::NoRepository => e
log_error('Invalid repository', e)
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')
project.repository.after_create
ensure
......@@ -54,5 +54,9 @@ module Geo
def repository
project.repository
end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end
end
......@@ -33,9 +33,11 @@ module Geo
ProjectWiki::CouldNotCreateWikiError,
Geo::EmptyCloneUrlPrefixError => e
log_error('Error syncing wiki repository', e)
registry.increment!(:wiki_retry_count)
rescue Gitlab::Git::Repository::NoRepository => 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
clean_up_temporary_repository if redownload
end
......@@ -47,5 +49,9 @@ module Geo
def repository
project.wiki.repository
end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end
end
- disable_key_edit = local_assigns.fetch(:disable_key_edit, false)
= 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
.col-sm-offset-2.col-sm-10
.checkbox
= form.label :primary do
= form.check_box :primary
%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.label :clone_protocol, s_('Geo|Repository cloning'), class: 'control-label'
......
- if current_user
.block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
%span.issuable-header-text.hide-collapsed.pull-left
Notifications
%button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span
{{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
.block.subscriptions
%subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
":subscribed" => "issue.subscribed",
":id" => "issue.id" }
---
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
# GitLab Geo repository sync worker
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
geo_repository_sync_worker:
cron: "*/5 * * * *"
cron: "*/1 * * * *"
# GitLab Geo file download dispatch worker
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
geo_file_download_dispatch_worker:
cron: "*/10 * * * *"
cron: "*/1 * * * *"
registry:
# 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 @@
#
# 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
enable_extension "plpgsql"
......@@ -879,6 +879,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
t.integer "repositories_changed_event_id", limit: 8
t.integer "repository_created_event_id", limit: 8
t.integer "hashed_storage_migrated_event_id", limit: 8
t.integer "lfs_object_deleted_event_id", limit: 8
end
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
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|
t.integer "geo_node_id", null: false
t.integer "namespace_id", null: false
......@@ -1366,7 +1375,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
create_table "merge_requests", force: :cascade do |t|
t.string "target_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 "assignee_id"
t.string "title"
......@@ -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", ["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", ["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", ["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
......@@ -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", ["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", ["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|
t.integer "merge_request_id", null: false
......@@ -2431,6 +2442,7 @@ ActiveRecord::Schema.define(version: 20171114104051) do
add_foreign_key "gcp_clusters", "services", 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_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_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
......@@ -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_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", "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", "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", "merge_requests", 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:
- [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.
- [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.
- **(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.
......
......@@ -620,6 +620,24 @@ the previous section:
the `gitlab` database user
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
### Consul and PostgreSQL changes not taking effect.
......
......@@ -268,9 +268,8 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
# Enable the master role and disable all other services in the machine
# (you can still enable Sentinel).
redis_master_role['enable'] = true
# Specify server role as 'redis_master_role'
roles ['redis_master_role']
# 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.
......@@ -286,6 +285,7 @@ The prerequisites for a HA Redis setup are the following:
redis['password'] = 'redis-password-goes-here'
```
1. Only the primary GitLab application server should handle migrations. To
prevent database migrations from running on upgrade, add the following
configuration to your `/etc/gitlab/gitlab.rb` file:
......@@ -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.
> 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
1. SSH into the **slave** Redis server.
......@@ -308,10 +312,8 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
# Enable the slave role and disable all other services in the machine
# (you can still enable Sentinel). This will also set automatically
# `redis['master'] = false`.
redis_slave_role['enable'] = true
# Specify server role as 'redis_slave_role'
roles ['redis_slave_role']
# 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.
......@@ -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. 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
......@@ -392,7 +398,7 @@ multiple machines with the Sentinel daemon.
be duplicate below):
```ruby
redis_sentinel_role['enable'] = true
roles ['redis_sentinel_role']
# Must be the same in every sentinel node
redis['master_name'] = 'gitlab-redis'
......
......@@ -8,8 +8,10 @@ To use a sprite Icon in HAML or Rails we use a specific helper function :
`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)
**css_class (optional)** If you want to add additional css classes
**Example**
......@@ -20,6 +22,24 @@ 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>`
### 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
Please use the following function inside JS to render an icon :
......@@ -29,7 +49,7 @@ Please use the following function inside JS to render an icon :
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
......
# 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:**
- GitLab Geo is part of [GitLab Enterprise Edition Premium][ee].
- 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
- 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
[minimum requirements](../install/requirements.md)
- Using GitLab Geo in combination with High Availability is considered **Beta**
>**Note:**
GitLab Geo changes significantly from release to release. Upgrades **are**
......@@ -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
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. [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. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration.md) to set the primary and secondary nodes.
......@@ -160,7 +155,7 @@ If you installed GitLab from source:
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. [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. [Configure SSH authorizations to use the database](ssh.md)
1. [Configure GitLab](configuration_source.md) to set the primary and secondary
......
......@@ -7,19 +7,7 @@ from source**](configuration_source.md) guide.
>**Note:**
Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages.
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"
Before attempting the steps in this stage, [complete all prior stages][toc].
This is the final step you need to follow in order to setup a Geo node.
......@@ -80,11 +68,11 @@ sensitive data in the database. Any secondary node must have the
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:
```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:
......@@ -101,16 +89,24 @@ running and accessible.
### 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**
(`/admin/application_settings`) in your browser
1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`:
![](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
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).
## Troubleshooting
See the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-omnibus-gitlab
......@@ -7,18 +7,7 @@ using the Omnibus GitLab packages, follow the
>**Note:**
Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages.
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"
Before attempting the steps in this stage, [complete all prior stages][toc].
This is the final step you need to follow in order to setup a Geo node.
......@@ -104,16 +93,29 @@ immediately. Make sure the secondary instance is running and accessible.
### 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**
(`/admin/application_settings`) in your browser
1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`:
![](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
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-
## Troubleshooting
Read the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-gitlab-installed-from-source
......@@ -7,19 +7,7 @@ from source, follow the
>**Note:**
Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages.
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"
Before attempting the steps in this stage, [complete all prior stages][toc].
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
......@@ -79,7 +67,7 @@ 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`.
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
password:
......@@ -165,7 +153,7 @@ will not be able to perform all necessary configuration steps. Refer to
```ruby
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['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
......@@ -214,7 +202,7 @@ will not be able to perform all necessary configuration steps. Refer to
```ruby
# Example configuration using internal IPs for a cloud configuration
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['md5_auth_cidr_addresses'] = ['10.1.10.5/32']
postgresql['max_replication_slots'] = 1 # Number of Geo secondary nodes
......@@ -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
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. 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.
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
take effect (`sudo gitlab-ctl restart postgresql` for Omnibus-provided PostgreSQL).
1. Save the file and [reconfigure GitLab][] for the database listen changes to
take effect.
**This step will fail.** This is caused by
[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
`netstat -plnt` to make sure that PostgreSQL is listening on port `5432` to
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
To prevent the secondary geo node trying to act as the primary once the
......@@ -329,14 +347,22 @@ primary before the database is replicated.
1. [Reconfigure GitLab][] for the changes to take effect.
1. Setup clock synchronization service in your Linux distro.
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).
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'
```
**IMPORTANT:** For Geo to work correctly, all nodes must be 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.
Refer to your Linux distribution documentation to setup clock
synchronization. This can easily be done using any NTP-compatible daemon.
### Step 4. Initiate the replication process
......@@ -357,19 +383,19 @@ data before running `pg_basebackup`.
sudo -i
```
1. New for 9.4: Choose a database-friendly name to use for your secondary to 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
the slot name.
1. New for 9.4: Choose a database-friendly name to use for your secondary to
use as the replication slot name. For example, if your domain is
`secondary.geo.example.com`, you may use `secondary_example` as the slot
name.
1. Execute the command below to start a backup/restore and begin the replication:
```
# 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
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.
......@@ -472,3 +498,4 @@ Read the [troubleshooting document](troubleshooting.md).
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[external postgresql]: #external-postgresql-instances
[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
>**Note:**
Stages of the setup process must be completed in the documented order.
Before attempting the steps in this stage, complete all prior stages.
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"
Before attempting the steps in this stage, [complete all prior stages][toc].
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
......@@ -197,16 +185,16 @@ The following guide assumes that:
1. Choose a database-friendly name to use for your secondary to 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
the slot name.
`secondary.geo.example.com`, you may use `secondary_example` as the slot
name.
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
-------------------------+---------------
geo_secondary_my_domain |
------------------+---------------
secondary_example |
(1 row)
```
......@@ -214,6 +202,23 @@ The following guide assumes that:
`netstat -plnt` to make sure that PostgreSQL is listening to 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
To prevent the secondary geo node trying to act as the primary once the
......@@ -307,19 +312,27 @@ primary before the database is replicated.
#### Enable tracking database on the secondary server
Geo secondary nodes use a tracking database to keep track of replication status and recover
automatically from some replication issues.
Geo secondary nodes use a tracking database to keep track of replication status
and recover automatically from some replication issues.
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
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. 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'
```
1. Setup clock synchronization service in your Linux distro.
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).
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
database. Note that GitLab will set up another database instance separate
......@@ -466,3 +479,4 @@ Read the [troubleshooting document](troubleshooting.md).
[pgback]: http://www.postgresql.org/docs/9.6/static/app-pgbasebackup.html
[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
[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.
## 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
> **Notes:**
......@@ -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.
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
Once you have created a group, you can manage its settings by navigating to
......
......@@ -151,16 +151,7 @@ group.
| View internal group epic | ✓ | ✓ | ✓ | ✓ | ✓ |
| View public group epic | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit 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.
| Delete group epic | | | | | ✓ |
### Subgroup permissions
......
......@@ -122,6 +122,7 @@
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
:show-delete-button="false"
:can-attach-file="false"
/>
</div>
<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
group_webhooks
issuable_default_templates
issue_board_focus_mode
issue_board_milestone
issue_weights
jenkins_integration
ldap_group_sync
......
......@@ -23,6 +23,9 @@
.panel-heading
Details
%ul.well-list
%li
%span.light Plan:
%strong= @license.plan.capitalize
%li
%span.light Uploaded:
%strong= time_ago_with_tooltip @license.created_at
......@@ -77,6 +80,7 @@
%tr
- @license.licensee.keys.each do |label|
%th= label
%th Plan
%th Uploaded at
%th Started at
%th Expired at
......@@ -86,6 +90,9 @@
%tr
- @license.licensee.keys.each do |label|
%td= license.licensee[label]
%td
%span
= license.plan.capitalize
%td
%span
= license.created_at
......
......@@ -108,7 +108,7 @@ module EE
end
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?
# Use the DN on record in GitLab when it's available
......@@ -127,8 +127,7 @@ module EE
end
def update_identity(dn, uid)
identity =
Identity.find_by(provider: provider, extern_uid: dn)
identity = ::Identity.with_extern_uid(provider, dn).take
# User may not exist in GitLab yet. Skip.
return unless identity.present?
......
......@@ -41,18 +41,14 @@ module Gitlab
batch.each do |event_log|
next unless can_replay?(event_log)
if event_log.repository_updated_event
handle_repository_updated(event_log)
elsif event_log.repository_created_event
handle_repository_created(event_log)
elsif event_log.repository_deleted_event
handle_repository_deleted(event_log)
elsif event_log.repositories_changed_event
handle_repositories_changed(event_log.repositories_changed_event)
elsif event_log.repository_renamed_event
handle_repository_renamed(event_log)
elsif event_log.hashed_storage_migrated_event
handle_hashed_storage_migrated(event_log)
begin
event = event_log.event
handler = "handle_#{event.class.name.demodulize.underscore}"
__send__(handler, event, event_log.created_at) # rubocop:disable GitlabSecurity/PublicSend
rescue NoMethodError => e
logger.error(e.message)
raise e
end
end
end
......@@ -85,12 +81,11 @@ module Gitlab
Gitlab::Geo.current_node&.projects_include?(event_log.project_id)
end
def handle_repository_created(event_log)
event = event_log.repository_created_event
def handle_repository_created_event(event, created_at)
registry = find_or_initialize_registry(event.project_id, resync_repository: true, resync_wiki: event.wiki_path.present?)
logger.event_info(
event_log.created_at,
created_at,
message: 'Repository created',
project_id: event.project_id,
repo_path: event.repo_path,
......@@ -103,12 +98,11 @@ module Gitlab
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end
def handle_repository_updated(event_log)
event = event_log.repository_updated_event
def handle_repository_updated_event(event, created_at)
registry = find_or_initialize_registry(event.project_id, "resync_#{event.source}" => true)
logger.event_info(
event_log.created_at,
created_at,
message: 'Repository update',
project_id: event.project_id,
source: event.source,
......@@ -120,15 +114,13 @@ module Gitlab
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end
def handle_repository_deleted(event_log)
event = event_log.repository_deleted_event
def handle_repository_deleted_event(event, created_at)
job_id = ::Geo::RepositoryDestroyService
.new(event.project_id, event.deleted_project_name, event.deleted_path, event.repository_storage_name)
.async_execute
logger.event_info(
event_log.created_at,
created_at,
message: 'Deleted project',
project_id: event.project_id,
repository_storage_name: event.repository_storage_name,
......@@ -139,7 +131,7 @@ module Gitlab
::Geo::ProjectRegistry.where(project_id: event.project_id).delete_all
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
job_id = ::Geo::RepositoriesCleanUpWorker.perform_in(1.hour, event.geo_node_id)
......@@ -151,8 +143,7 @@ module Gitlab
end
end
def handle_repository_renamed(event_log)
event = event_log.repository_renamed_event
def handle_repository_renamed_event(event, created_at)
return unless event.project_id
old_path = event.old_path_with_namespace
......@@ -163,7 +154,7 @@ module Gitlab
.async_execute
logger.event_info(
event_log.created_at,
created_at,
message: 'Renaming project',
project_id: event.project_id,
old_path: old_path,
......@@ -171,8 +162,7 @@ module Gitlab
job_id: job_id)
end
def handle_hashed_storage_migrated(event_log)
event = event_log.hashed_storage_migrated_event
def handle_hashed_storage_migrated_event(event, created_at)
return unless event.project_id
job_id = ::Geo::HashedStorageMigrationService.new(
......@@ -183,7 +173,7 @@ module Gitlab
).async_execute
logger.event_info(
event_log.created_at,
created_at,
message: 'Migrating project to hashed storage',
project_id: event.project_id,
old_storage_version: event.old_storage_version,
......@@ -193,6 +183,22 @@ module Gitlab
job_id: job_id)
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)
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
registry.assign_attributes(attrs)
......
module Gitlab
module ImportExport
class MergeRequestParser
FORKED_PROJECT_ID = -1
FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project
......
......@@ -44,7 +44,7 @@ module Gitlab
private
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
end
end
......
......@@ -5,7 +5,7 @@
module Gitlab
module ShellAdapter
def gitlab_shell
Gitlab::Shell.new
@gitlab_shell ||= Gitlab::Shell.new
end
end
end
......@@ -8,6 +8,13 @@ ENV DEBIAN_FRONTEND noninteractive
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
##
# 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
#
......
......@@ -49,6 +49,10 @@ module QA
module Sandbox
autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
end
module Admin
autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage'
end
end
end
......@@ -64,9 +68,11 @@ module QA
autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
end
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
end
......@@ -82,6 +88,7 @@ module QA
module Admin
autoload :Menu, 'qa/page/admin/menu'
autoload :Settings, 'qa/page/admin/settings'
end
module Mattermost
......@@ -97,6 +104,13 @@ module QA
autoload :Repository, 'qa/git/repository'
end
##
# Classes describing shell interaction with GitLab
#
module Shell
autoload :Omnibus, 'qa/shell/omnibus'
end
##
# Classes that make it possible to execute features tests.
#
......
......@@ -6,10 +6,19 @@ module QA
module Page
module Admin
autoload :License, 'qa/ee/page/admin/license'
autoload :GeoNodes, 'qa/ee/page/admin/geo_nodes'
end
end
module Scenario
module Geo
autoload :Node, 'qa/ee/scenario/geo/node'
end
module Test
autoload :Geo, 'qa/ee/scenario/test/geo'
end
module License
autoload :Add, 'qa/ee/scenario/license/add'
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
require 'qa/ee'
end
##
# TODO generic solution for screenshot in factories
#
# gitlab-org/gitlab-qa#86
#
def perform_before_hooks
return unless ENV['EE_LICENSE']
......
......@@ -2,9 +2,16 @@ module QA
module Page
module Admin
class Menu < Page::Base
def go_to_geo_nodes
click_link 'Geo Nodes'
end
def go_to_license
link = find_link 'License'
link.click
click_link 'License'
end
def go_to_settings
click_link 'Settings'
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 Page
class Base
......@@ -7,6 +9,21 @@ module QA
def refresh
visit current_url
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
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
#
start = Time.now
while Time.now - start < 240
while Time.now - start < 1000
break if page.has_css?('.application', wait: 10)
refresh
......
......@@ -7,7 +7,10 @@ module QA
end
def go_to_projects
within_top_menu { click_link 'Projects' }
within_top_menu do
click_link 'Projects'
click_link 'Your projects'
end
end
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
find('#project_clone').value
end
def project_name
find('.project-title').text
end
def wait_for_push
sleep 5
end
......
......@@ -28,7 +28,7 @@ module QA
private
def attribute(name, arg, desc)
def attribute(name, arg, desc = '')
options.push(Option.new(name, arg, desc))
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