Commit 78e34e45 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents e189d788 02fcb882
...@@ -132,9 +132,6 @@ export default { ...@@ -132,9 +132,6 @@ export default {
noteable: this.noteableDisplayName, noteable: this.noteableDisplayName,
}); });
}, },
buttonVariant() {
return this.isOpen ? 'warning' : 'default';
},
actionButtonClassNames() { actionButtonClassNames() {
return { return {
'btn-reopen': !this.isOpen, 'btn-reopen': !this.isOpen,
...@@ -422,8 +419,6 @@ export default { ...@@ -422,8 +419,6 @@ export default {
<gl-button <gl-button
v-if="canToggleIssueState" v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading" :loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
:class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']" :class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']"
:disabled="isSubmitting" :disabled="isSubmitting"
data-testid="close-reopen-button" data-testid="close-reopen-button"
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants'; import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import initInviteMembersBanner from '~/groups/init_invite_members_banner'; import initInviteMembersBanner from '~/groups/init_invite_members_banner';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { getPagePath, getDashPath } from '~/lib/utils/common_utils'; import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications'; import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list'; import ProjectsList from '~/projects_list';
...@@ -26,6 +24,4 @@ export default function initGroupDetails(actionName = 'show') { ...@@ -26,6 +24,4 @@ export default function initGroupDetails(actionName = 'show') {
new ProjectsList(); new ProjectsList();
initInviteMembersBanner(); initInviteMembersBanner();
initInviteMembersModal();
initInviteMembersTrigger();
} }
...@@ -3,8 +3,6 @@ import Activities from '~/activities'; ...@@ -3,8 +3,6 @@ import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import { initUploadForm } from '~/blob_edit/blob_bundle'; import { initUploadForm } from '~/blob_edit/blob_bundle';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url'; import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications'; import initVueNotificationsDropdown from '~/notifications';
import initReadMore from '~/read_more'; import initReadMore from '~/read_more';
...@@ -43,6 +41,3 @@ leaveByUrl('project'); ...@@ -43,6 +41,3 @@ leaveByUrl('project');
initVueNotificationsDropdown(); initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
initInviteMembersTrigger();
initInviteMembersModal();
...@@ -9,10 +9,6 @@ import { ...@@ -9,10 +9,6 @@ import {
GlFormSelect, GlFormSelect,
GlFormTextarea, GlFormTextarea,
GlLink, GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf, GlSprintf,
GlLoadingIcon, GlLoadingIcon,
GlSafeHtmlDirective as SafeHtml, GlSafeHtmlDirective as SafeHtml,
...@@ -26,19 +22,26 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -26,19 +22,26 @@ import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale'; import { s__, __, n__ } from '~/locale';
import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants'; import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
import RefsDropdown from './refs_dropdown.vue';
const i18n = {
variablesDescription: s__(
'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
),
defaultError: __('Something went wrong on our end. Please try again.'),
refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'),
submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'),
warningTitle: __('The form contains the following warning:'),
maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
};
export default { export default {
typeOptions: [ typeOptions: [
{ value: VARIABLE_TYPE, text: __('Variable') }, { value: VARIABLE_TYPE, text: __('Variable') },
{ value: FILE_TYPE, text: __('File') }, { value: FILE_TYPE, text: __('File') },
], ],
variablesDescription: s__( i18n,
'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
),
formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0', formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
errorTitle: __('Pipeline cannot be run.'),
warningTitle: __('The form contains the following warning:'),
maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
// this height value is used inline on the textarea to match the input field height // this height value is used inline on the textarea to match the input field height
// it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
textAreaStyle: { height: '32px' }, textAreaStyle: { height: '32px' },
...@@ -52,12 +55,9 @@ export default { ...@@ -52,12 +55,9 @@ export default {
GlFormSelect, GlFormSelect,
GlFormTextarea, GlFormTextarea,
GlLink, GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlSprintf, GlSprintf,
GlLoadingIcon, GlLoadingIcon,
RefsDropdown,
}, },
directives: { SafeHtml }, directives: { SafeHtml },
props: { props: {
...@@ -77,14 +77,6 @@ export default { ...@@ -77,14 +77,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
branches: {
type: Array,
required: true,
},
tags: {
type: Array,
required: true,
},
settingsLink: { settingsLink: {
type: String, type: String,
required: true, required: true,
...@@ -111,11 +103,11 @@ export default { ...@@ -111,11 +103,11 @@ export default {
}, },
data() { data() {
return { return {
searchTerm: '',
refValue: { refValue: {
shortName: this.refParam, shortName: this.refParam,
}, },
form: {}, form: {},
errorTitle: null,
error: null, error: null,
warnings: [], warnings: [],
totalWarnings: 0, totalWarnings: 0,
...@@ -125,22 +117,6 @@ export default { ...@@ -125,22 +117,6 @@ export default {
}; };
}, },
computed: { computed: {
lowerCasedSearchTerm() {
return this.searchTerm.toLowerCase();
},
filteredBranches() {
return this.branches.filter((branch) =>
branch.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
);
},
filteredTags() {
return this.tags.filter((tag) =>
tag.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
);
},
hasTags() {
return this.tags.length > 0;
},
overMaxWarningsLimit() { overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings; return this.totalWarnings > this.maxWarnings;
}, },
...@@ -148,7 +124,7 @@ export default { ...@@ -148,7 +124,7 @@ export default {
return n__('%d warning found:', '%d warnings found:', this.warnings.length); return n__('%d warning found:', '%d warnings found:', this.warnings.length);
}, },
summaryMessage() { summaryMessage() {
return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary; return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
}, },
shouldShowWarning() { shouldShowWarning() {
return this.warnings.length > 0 && !this.isWarningDismissed; return this.warnings.length > 0 && !this.isWarningDismissed;
...@@ -166,6 +142,11 @@ export default { ...@@ -166,6 +142,11 @@ export default {
return this.form[this.refFullName]?.descriptions ?? {}; return this.form[this.refFullName]?.descriptions ?? {};
}, },
}, },
watch: {
refValue() {
this.loadConfigVariablesForm();
},
},
created() { created() {
// this is needed until we add support for ref type in url query strings // this is needed until we add support for ref type in url query strings
// ensure default branch is called with full ref on load // ensure default branch is called with full ref on load
...@@ -174,7 +155,7 @@ export default { ...@@ -174,7 +155,7 @@ export default {
this.refValue.fullName = `refs/heads/${this.refValue.shortName}`; this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
} }
this.setRefSelected(this.refValue); this.loadConfigVariablesForm();
}, },
methods: { methods: {
addEmptyVariable(refValue) { addEmptyVariable(refValue) {
...@@ -213,49 +194,47 @@ export default { ...@@ -213,49 +194,47 @@ export default {
this.setVariable(refValue, type, key, value); this.setVariable(refValue, type, key, value);
}); });
}, },
setRefSelected(refValue) {
this.refValue = refValue;
if (!this.form[this.refFullName]) {
this.fetchConfigVariables(this.refFullName || this.refShortName)
.then(({ descriptions, params }) => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions,
});
// Add default variables from yml
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
})
.catch(() => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions: {},
});
})
.finally(() => {
// Add/update variables, e.g. from query string
if (this.variableParams) {
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
}
// Adds empty var at the end of the form
this.addEmptyVariable(this.refFullName);
});
}
},
isSelected(ref) {
return ref.fullName === this.refValue.fullName;
},
removeVariable(index) { removeVariable(index) {
this.variables.splice(index, 1); this.variables.splice(index, 1);
}, },
canRemove(index) { canRemove(index) {
return index < this.variables.length - 1; return index < this.variables.length - 1;
}, },
loadConfigVariablesForm() {
// Skip when variables already cached in `form`
if (this.form[this.refFullName]) {
return;
}
this.fetchConfigVariables(this.refFullName || this.refShortName)
.then(({ descriptions, params }) => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions,
});
// Add default variables from yml
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
})
.catch(() => {
Vue.set(this.form, this.refFullName, {
variables: [],
descriptions: {},
});
})
.finally(() => {
// Add/update variables, e.g. from query string
if (this.variableParams) {
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
}
// Adds empty var at the end of the form
this.addEmptyVariable(this.refFullName);
});
},
fetchConfigVariables(refValue) { fetchConfigVariables(refValue) {
this.isLoading = true; this.isLoading = true;
...@@ -330,11 +309,25 @@ export default { ...@@ -330,11 +309,25 @@ export default {
} = err?.response?.data; } = err?.response?.data;
const [error] = errors; const [error] = errors;
this.error = error; this.reportError({
this.warnings = warnings; title: i18n.submitErrorTitle,
this.totalWarnings = totalWarnings; error,
warnings,
totalWarnings,
});
}); });
}, },
onRefsLoadingError(error) {
this.reportError({ title: i18n.refsLoadingErrorTitle });
Sentry.captureException(error);
},
reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) {
this.errorTitle = title;
this.error = error;
this.warnings = warnings;
this.totalWarnings = totalWarnings;
},
}, },
}; };
</script> </script>
...@@ -343,7 +336,7 @@ export default { ...@@ -343,7 +336,7 @@ export default {
<gl-form @submit.prevent="createPipeline"> <gl-form @submit.prevent="createPipeline">
<gl-alert <gl-alert
v-if="error" v-if="error"
:title="$options.errorTitle" :title="errorTitle"
:dismissible="false" :dismissible="false"
variant="danger" variant="danger"
class="gl-mb-4" class="gl-mb-4"
...@@ -353,7 +346,7 @@ export default { ...@@ -353,7 +346,7 @@ export default {
</gl-alert> </gl-alert>
<gl-alert <gl-alert
v-if="shouldShowWarning" v-if="shouldShowWarning"
:title="$options.warningTitle" :title="$options.i18n.warningTitle"
variant="warning" variant="warning"
class="gl-mb-4" class="gl-mb-4"
data-testid="run-pipeline-warning-alert" data-testid="run-pipeline-warning-alert"
...@@ -380,31 +373,7 @@ export default { ...@@ -380,31 +373,7 @@ export default {
</details> </details>
</gl-alert> </gl-alert>
<gl-form-group :label="s__('Pipeline|Run for branch name or tag')"> <gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
<gl-dropdown :text="refShortName" block> <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
<gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search refs')" />
<gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="branch in filteredBranches"
:key="branch.fullName"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(branch)"
@click="setRefSelected(branch)"
>
{{ branch.shortName }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="tag in filteredTags"
:key="tag.fullName"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(tag)"
@click="setRefSelected(tag)"
>
{{ tag.shortName }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group> </gl-form-group>
<gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" /> <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
...@@ -465,7 +434,7 @@ export default { ...@@ -465,7 +434,7 @@ export default {
</div> </div>
<template #description <template #description
><gl-sprintf :message="$options.variablesDescription"> ><gl-sprintf :message="$options.i18n.variablesDescription">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="settingsLink">{{ content }}</gl-link> <gl-link :href="settingsLink">{{ content }}</gl-link>
</template> </template>
......
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { BRANCH_REF_TYPE, TAG_REF_TYPE, DEBOUNCE_REFS_SEARCH_MS } from '../constants';
import formatRefs from '../utils/format_refs';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
},
inject: ['projectRefsEndpoint'],
props: {
value: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
isLoading: false,
searchTerm: '',
branches: [],
tags: [],
};
},
computed: {
lowerCasedSearchTerm() {
return this.searchTerm.toLowerCase();
},
refShortName() {
return this.value.shortName;
},
hasTags() {
return this.tags.length > 0;
},
},
watch: {
searchTerm() {
this.debouncedLoadRefs();
},
},
methods: {
loadRefs() {
this.isLoading = true;
axios
.get(this.projectRefsEndpoint, {
params: {
search: this.lowerCasedSearchTerm,
},
})
.then(({ data }) => {
// Note: These keys are uppercase in API
const { Branches = [], Tags = [] } = data;
this.branches = formatRefs(Branches, BRANCH_REF_TYPE);
this.tags = formatRefs(Tags, TAG_REF_TYPE);
})
.catch((e) => {
this.$emit('loadingError', e);
})
.finally(() => {
this.isLoading = false;
});
},
debouncedLoadRefs: debounce(function debouncedLoadRefs() {
this.loadRefs();
}, DEBOUNCE_REFS_SEARCH_MS),
setRefSelected(ref) {
this.$emit('input', ref);
},
isSelected(ref) {
return ref.fullName === this.value.fullName;
},
},
};
</script>
<template>
<gl-dropdown :text="refShortName" block @show.once="loadRefs">
<gl-search-box-by-type
v-model.trim="searchTerm"
:is-loading="isLoading"
:placeholder="__('Search refs')"
/>
<gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="branch in branches"
:key="branch.fullName"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(branch)"
@click="setRefSelected(branch)"
>
{{ branch.shortName }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="tag in tags"
:key="tag.fullName"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(tag)"
@click="setRefSelected(tag)"
>
{{ tag.shortName }}
</gl-dropdown-item>
</gl-dropdown>
</template>
export const VARIABLE_TYPE = 'env_var'; export const VARIABLE_TYPE = 'env_var';
export const FILE_TYPE = 'file'; export const FILE_TYPE = 'file';
export const DEBOUNCE_REFS_SEARCH_MS = 250;
export const CONFIG_VARIABLES_TIMEOUT = 5000; export const CONFIG_VARIABLES_TIMEOUT = 5000;
export const BRANCH_REF_TYPE = 'branch'; export const BRANCH_REF_TYPE = 'branch';
export const TAG_REF_TYPE = 'tag'; export const TAG_REF_TYPE = 'tag';
import Vue from 'vue'; import Vue from 'vue';
import PipelineNewForm from './components/pipeline_new_form.vue'; import PipelineNewForm from './components/pipeline_new_form.vue';
import formatRefs from './utils/format_refs';
export default () => { export default () => {
const el = document.getElementById('js-new-pipeline'); const el = document.getElementById('js-new-pipeline');
const { const {
// provide/inject
projectRefsEndpoint,
// props
projectId, projectId,
pipelinesPath, pipelinesPath,
configVariablesPath, configVariablesPath,
...@@ -12,19 +15,18 @@ export default () => { ...@@ -12,19 +15,18 @@ export default () => {
refParam, refParam,
varParam, varParam,
fileParam, fileParam,
branchRefs,
tagRefs,
settingsLink, settingsLink,
maxWarnings, maxWarnings,
} = el?.dataset; } = el?.dataset;
const variableParams = JSON.parse(varParam); const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam); const fileParams = JSON.parse(fileParam);
const branches = formatRefs(JSON.parse(branchRefs), 'branch');
const tags = formatRefs(JSON.parse(tagRefs), 'tag');
return new Vue({ return new Vue({
el, el,
provide: {
projectRefsEndpoint,
},
render(createElement) { render(createElement) {
return createElement(PipelineNewForm, { return createElement(PipelineNewForm, {
props: { props: {
...@@ -35,8 +37,6 @@ export default () => { ...@@ -35,8 +37,6 @@ export default () => {
refParam, refParam,
variableParams, variableParams,
fileParams, fileParams,
branches,
tags,
settingsLink, settingsLink,
maxWarnings: Number(maxWarnings), maxWarnings: Number(maxWarnings),
}, },
......
...@@ -196,10 +196,6 @@ ...@@ -196,10 +196,6 @@
@include btn-orange; @include btn-orange;
} }
&.btn-close {
@include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700);
}
&.btn-danger { &.btn-danger {
@include btn-red; @include btn-red;
} }
......
...@@ -16,11 +16,6 @@ ...@@ -16,11 +16,6 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
= content_for :invite_members_sidebar do
- if can_invite_members_for_group?(@group)
%li
.js-invite-members-trigger{ data: { icon: 'plus', classes: 'gl-text-decoration-none! gl-shadow-none!', display_text: _('Invite team members') } }
= render partial: 'flash_messages' = render partial: 'flash_messages'
= render_if_exists 'trials/banner', namespace: @group = render_if_exists 'trials/banner', namespace: @group
......
...@@ -137,8 +137,6 @@ ...@@ -137,8 +137,6 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Members') = _('Members')
= content_for :invite_members_sidebar
- if group_sidebar_link?(:settings) - if group_sidebar_link?(:settings)
= nav_link(path: group_settings_nav_link_paths) do = nav_link(path: group_settings_nav_link_paths) do
= link_to edit_group_path(@group) do = link_to edit_group_path(@group) do
......
...@@ -378,8 +378,6 @@ ...@@ -378,8 +378,6 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Members') = _('Members')
= content_for :invite_members_sidebar
- if project_nav_tab? :settings - if project_nav_tab? :settings
= nav_link(path: sidebar_settings_paths) do = nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project) do = link_to edit_project_path(@project) do
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
- max_project_topic_length = 15 - max_project_topic_length = 15
- emails_disabled = @project.emails_disabled? - emails_disabled = @project.emails_disabled?
= render 'projects/invite_members_modal', project: @project
.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] } .project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
.row.gl-mb-3 .row.gl-mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex .home-panel-title-row.col-md-12.col-lg-6.d-flex
......
- return unless can_invite_members_for_project?(@project)
%li
.js-invite-members-trigger{ data: { icon: 'plus', classes: 'gl-text-decoration-none! gl-shadow-none!', display_text: _('Invite team members') } }
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
- default_branch_name = @project.default_branch_or_master - default_branch_name = @project.default_branch_or_master
- @skip_current_level_breadcrumb = true - @skip_current_level_breadcrumb = true
= content_for :invite_members_sidebar do
= render partial: 'projects/invite_members_link'
= render partial: 'flash_messages', locals: { project: @project } = render partial: 'flash_messages', locals: { project: @project }
= render "home_panel" = render "home_panel"
......
...@@ -12,8 +12,6 @@ ...@@ -12,8 +12,6 @@
#{ _('This means you can not push code until you create an empty repository or import existing one.') } #{ _('This means you can not push code until you create an empty repository or import existing one.') }
%hr %hr
= render 'projects/invite_members_modal', project: @project
.no-repo-actions .no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn gl-button btn-confirm' do = link_to project_repository_path(@project), method: :post, class: 'btn gl-button btn-confirm' do
#{ _('Create empty repository') } #{ _('Create empty repository') }
......
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
ref_param: params[:ref] || @project.default_branch, ref_param: params[:ref] || @project.default_branch,
var_param: params[:var].to_json, var_param: params[:var].to_json,
file_param: params[:file_var].to_json, file_param: params[:file_var].to_json,
branch_refs: @project.repository.branch_names.to_json.html_safe, project_refs_endpoint: refs_project_path(@project, sort: 'updated_desc'),
tag_refs: @project.repository.tag_names.to_json.html_safe,
settings_link: project_settings_ci_cd_path(@project), settings_link: project_settings_ci_cd_path(@project),
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } } max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
......
...@@ -6,9 +6,6 @@ ...@@ -6,9 +6,6 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= content_for :invite_members_sidebar do
= render partial: 'projects/invite_members_link'
= render partial: 'flash_messages', locals: { project: @project } = render partial: 'flash_messages', locals: { project: @project }
= render "projects/last_push" = render "projects/last_push"
......
---
title: Deemphasize comment and close button
merge_request: 55075
author:
type: other
---
title: Improve performance of manual pipeline form by limiting the refs loaded on page load.
merge_request: 55394
author:
type: performance
---
title: Fix N+1 queries in api/v3/repos/:namespace/:project/events endpoint
merge_request: 55442
author:
type: performance
--- ---
name: api_v3_repos_events_optimization name: new_route_ci_minutes_purchase
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54618 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54934
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322059 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322582
milestone: '13.10' milestone: '13.10'
type: development type: development
group: group::ecosystem group: group::purchase
default_enabled: false default_enabled: false
...@@ -17,21 +17,21 @@ full list of reference architectures, see ...@@ -17,21 +17,21 @@ full list of reference architectures, see
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------| |--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | `m5.2xlarge` | D8s v3 | | PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Gitaly Cluster | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | `m5.4xlarge` | D16s v3 | | Gitaly | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
| Praefect | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Praefect | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | `c5.9xlarge` | F32s v2 | | GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a | | Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 |
...@@ -206,7 +206,7 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -206,7 +206,7 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.111`: GitLab application 1 - `10.6.0.111`: GitLab application 1
- `10.6.0.112`: GitLab application 2 - `10.6.0.112`: GitLab application 2
- `10.6.0.113`: GitLab application 3 - `10.6.0.113`: GitLab application 3
- `10.6.0.121`: Prometheus - `10.6.0.151`: Prometheus
## Configure the external load balancer ## Configure the external load balancer
...@@ -1927,7 +1927,7 @@ To configure the Sidekiq nodes, on each one: ...@@ -1927,7 +1927,7 @@ To configure the Sidekiq nodes, on each one:
node_exporter['listen_address'] = '0.0.0.0:9100' node_exporter['listen_address'] = '0.0.0.0:9100'
# Rails Status for prometheus # Rails Status for prometheus
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -2055,8 +2055,8 @@ On each node perform the following: ...@@ -2055,8 +2055,8 @@ On each node perform the following:
# Add the monitoring node's IP address to the monitoring whitelist and allow it to # Add the monitoring node's IP address to the monitoring whitelist and allow it to
# scrape the NGINX metrics # scrape the NGINX metrics
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -2192,7 +2192,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and ...@@ -2192,7 +2192,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and
The following IP will be used as an example: The following IP will be used as an example:
- `10.6.0.121`: Prometheus - `10.6.0.151`: Prometheus
To configure the Monitoring node: To configure the Monitoring node:
......
...@@ -12,100 +12,110 @@ full list of reference architectures, see ...@@ -12,100 +12,110 @@ full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 25,000 > - **Supported users (approximate):** 25,000
> - **High Availability:** Yes > - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS > - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git (Pull): 50 RPS, Git (Push): 10 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------| |-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | External load balancing node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | `m5.2xlarge` | D8s v3 | | PostgreSQL | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | `c5.large` | F2s v2 | | Internal load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Gitaly | 2 (minimum) | 32 vCPU, 120 GB memory | n1-standard-32 | `m5.8xlarge` | D32s v3 | | Gitaly | 3 | 32 vCPU, 120 GB memory | n1-standard-32 | m5.8xlarge | D32s v3 |
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Praefect | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| GitLab Rails | 5 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | `c5.9xlarge` | F32s v2 | | Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 5 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a | | Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
```mermaid ```plantuml
stateDiagram-v2 @startuml 25k
[*] --> LoadBalancer card "**External Load Balancer**" as elb #6a9be7
LoadBalancer --> ApplicationServer card "**Internal Load Balancer**" as ilb #9370DB
ApplicationServer --> BackgroundJobs together {
ApplicationServer --> Gitaly collections "**GitLab Rails** x5" as gitlab #32CD32
ApplicationServer --> Redis_Cache collections "**Sidekiq** x4" as sidekiq #ff8dd1
ApplicationServer --> Redis_Queues }
ApplicationServer --> PgBouncer
PgBouncer --> Database together {
ApplicationServer --> ObjectStorage card "**Prometheus + Grafana**" as monitor #7FFFD4
BackgroundJobs --> ObjectStorage collections "**Consul** x3" as consul #e76a9b
}
ApplicationMonitoring -->ApplicationServer
ApplicationMonitoring -->PgBouncer card "Gitaly Cluster" as gitaly_cluster {
ApplicationMonitoring -->Database collections "**Praefect** x3" as praefect #FF8C00
ApplicationMonitoring -->BackgroundJobs collections "**Gitaly** x3" as gitaly #FF8C00
card "**Praefect PostgreSQL***\n//Non fault-tolerant//" as praefect_postgres #FF8C00
ApplicationServer --> Consul
praefect -[#FF8C00]-> gitaly
Consul --> Database praefect -[#FF8C00]> praefect_postgres
Consul --> PgBouncer }
Redis_Cache --> Consul
Redis_Queues --> Consul card "Database" as database {
BackgroundJobs --> Consul collections "**PGBouncer** x3" as pgbouncer #4EA7FF
card "**PostgreSQL** (Primary)" as postgres_primary #4EA7FF
state Consul { collections "**PostgreSQL** (Secondary) x2" as postgres_secondary #4EA7FF
"Consul_1..3"
} pgbouncer -[#4EA7FF]-> postgres_primary
postgres_primary .[#4EA7FF]> postgres_secondary
state Database { }
"PG_Primary_Node"
"PG_Secondary_Node_1..2" card "redis" as redis {
} collections "**Redis Persistent** x3" as redis_persistent #FF6347
collections "**Redis Cache** x3" as redis_cache #FF6347
state Redis_Cache { collections "**Redis Persistent Sentinel** x3" as redis_persistent_sentinel #FF6347
"R_Cache_Primary_Node" collections "**Redis Cache Sentinel** x3"as redis_cache_sentinel #FF6347
"R_Cache_Replica_Node_1..2"
"R_Cache_Sentinel_1..3" redis_persistent <.[#FF6347]- redis_persistent_sentinel
} redis_cache <.[#FF6347]- redis_cache_sentinel
}
state Redis_Queues {
"R_Queues_Primary_Node" cloud "**Object Storage**" as object_storage #white
"R_Queues_Replica_Node_1..2"
"R_Queues_Sentinel_1..3" elb -[#6a9be7]-> gitlab
} elb -[#6a9be7]--> monitor
state Gitaly { gitlab -[#32CD32]> sidekiq
"Gitaly_1..2" gitlab -[#32CD32]--> ilb
} gitlab -[#32CD32]-> object_storage
gitlab -[#32CD32]---> redis
state BackgroundJobs { gitlab -[hidden]-> monitor
"Sidekiq_1..4" gitlab -[hidden]-> consul
}
sidekiq -[#ff8dd1]--> ilb
state ApplicationServer { sidekiq -[#ff8dd1]-> object_storage
"GitLab_Rails_1..5" sidekiq -[#ff8dd1]---> redis
} sidekiq -[hidden]-> monitor
sidekiq -[hidden]-> consul
state LoadBalancer {
"LoadBalancer_1" ilb -[#9370DB]-> gitaly_cluster
} ilb -[#9370DB]-> database
state ApplicationMonitoring { consul .[#e76a9b]u-> gitlab
"Prometheus" consul .[#e76a9b]u-> sidekiq
"Grafana" consul .[#e76a9b]> monitor
} consul .[#e76a9b]-> database
consul .[#e76a9b]-> gitaly_cluster
state PgBouncer { consul .[#e76a9b,norank]--> redis
"Internal_Load_Balancer"
"PgBouncer_1..3" monitor .[#7FFFD4]u-> gitlab
} monitor .[#7FFFD4]u-> sidekiq
monitor .[#7FFFD4]> consul
monitor .[#7FFFD4]-> database
monitor .[#7FFFD4]-> gitaly_cluster
monitor .[#7FFFD4,norank]--> redis
monitor .[#7FFFD4]> ilb
monitor .[#7FFFD4,norank]u--> elb
@enduml
``` ```
The Google Cloud Platform (GCP) architectures were built and tested using the The Google Cloud Platform (GCP) architectures were built and tested using the
...@@ -120,19 +130,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object- ...@@ -120,19 +130,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object-
is recommended instead of using NFS. Using an object storage service also is recommended instead of using NFS. Using an object storage service also
doesn't require you to provision and maintain a node. doesn't require you to provision and maintain a node.
It's also worth noting that at this time [Praefect requires its own database server](../gitaly/praefect.md#postgresql) and
that to achieve full High Availability a third party PostgreSQL database solution will be required.
We hope to offer a built in solutions for these restrictions in the future but in the meantime a non HA PostgreSQL server
can be set up via Omnibus GitLab, which the above specs reflect. Refer to the following issues for more information: [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919) & [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398)
## Setup components ## Setup components
To set up GitLab and its components to accommodate up to 25,000 users: To set up GitLab and its components to accommodate up to 25,000 users:
1. [Configure the external load balancing node](#configure-the-external-load-balancer) 1. [Configure the external load balancer](#configure-the-external-load-balancer)
to handle the load balancing of the GitLab application services nodes. to handle the load balancing of the GitLab application services nodes.
1. [Configure the internal load balancer](#configure-the-internal-load-balancer).
to handle the load balancing of GitLab application internal connections.
1. [Configure Consul](#configure-consul). 1. [Configure Consul](#configure-consul).
1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab. 1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab.
1. [Configure PgBouncer](#configure-pgbouncer). 1. [Configure PgBouncer](#configure-pgbouncer).
1. [Configure the internal load balancing node](#configure-the-internal-load-balancer).
1. [Configure Redis](#configure-redis). 1. [Configure Redis](#configure-redis).
1. [Configure Gitaly](#configure-gitaly), 1. [Configure Gitaly Cluster](#configure-gitaly-cluster),
which provides access to the Git repositories. provides access to the Git repositories.
1. [Configure Sidekiq](#configure-sidekiq). 1. [Configure Sidekiq](#configure-sidekiq).
1. [Configure the main GitLab Rails application](#configure-gitlab-rails) 1. [Configure the main GitLab Rails application](#configure-gitlab-rails)
to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend
...@@ -178,6 +194,11 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -178,6 +194,11 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.83`: Sentinel - Queues 3 - `10.6.0.83`: Sentinel - Queues 3
- `10.6.0.91`: Gitaly 1 - `10.6.0.91`: Gitaly 1
- `10.6.0.92`: Gitaly 2 - `10.6.0.92`: Gitaly 2
- `10.6.0.93`: Gitaly 3
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
- `10.6.0.141`: Praefect PostgreSQL 1 (non HA)
- `10.6.0.101`: Sidekiq 1 - `10.6.0.101`: Sidekiq 1
- `10.6.0.102`: Sidekiq 2 - `10.6.0.102`: Sidekiq 2
- `10.6.0.103`: Sidekiq 3 - `10.6.0.103`: Sidekiq 3
...@@ -185,7 +206,9 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -185,7 +206,9 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.111`: GitLab application 1 - `10.6.0.111`: GitLab application 1
- `10.6.0.112`: GitLab application 2 - `10.6.0.112`: GitLab application 2
- `10.6.0.113`: GitLab application 3 - `10.6.0.113`: GitLab application 3
- `10.6.0.121`: Prometheus - `10.6.0.114`: GitLab application 4
- `10.6.0.115`: GitLab application 5
- `10.6.0.151`: Prometheus
## Configure the external load balancer ## Configure the external load balancer
...@@ -308,6 +331,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ...@@ -308,6 +331,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
</a> </a>
</div> </div>
## Configure the internal load balancer
The Internal Load Balancer is used to balance any internal connections the GitLab environment requires
such as connections to [PgBouncer](#configure-pgbouncer) and [Praefect](#configure-praefect) (Gitaly Cluster).
Note that it's a separate node from the External Load Balancer and shouldn't have any access externally.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
frontend internal-praefect-tcp-in
bind *:2305
mode tcp
option tcplog
option clitcpka
default_backend praefect
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
backend praefect
mode tcp
option tcp-check
option srvtcpka
server praefect1 10.6.0.131:2305 check
server praefect2 10.6.0.132:2305 check
server praefect3 10.6.0.133:2305 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Consul ## Configure Consul
The following IPs will be used as an example: The following IPs will be used as an example:
...@@ -662,52 +750,6 @@ The following IPs will be used as an example: ...@@ -662,52 +750,6 @@ The following IPs will be used as an example:
</a> </a>
</div> </div>
### Configure the internal load balancer
If you're running more than one PgBouncer node as recommended, then at this time you'll need to set
up a TCP internal load balancer to serve each correctly.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Redis ## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica** Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
...@@ -1302,19 +1344,283 @@ To configure the Sentinel Queues server: ...@@ -1302,19 +1344,283 @@ To configure the Sentinel Queues server:
</a> </a>
</div> </div>
## Configure Gitaly ## Configure Gitaly Cluster
NOTE: [Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
[Gitaly Cluster](../gitaly/praefect.md) support In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified The recommended cluster setup includes the following components:
some Architecture specs will likely change as a result to support the new
and improved designed. - 3 Gitaly nodes: Replicated storage of Git repositories.
- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
is required for Praefect database connections to be made highly available.
- 1 load balancer: A load balancer is required for Praefect. The
[internal load balancer](#configure-the-internal-load-balancer) will be used.
This section will detail how to configure the recommended standard setup in order.
For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
### Configure Praefect PostgreSQL
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
The following IPs will be used as an example:
- `10.6.0.141`: Praefect PostgreSQL
First, make sure to [install](https://about.gitlab.com/install/)
the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
install the necessary dependencies from step 1, and add the
GitLab package repository from step 2. When installing GitLab
in the second step, do not supply the `EXTERNAL_URL` value.
1. SSH in to the Praefect PostgreSQL node.
1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as `<praefect_postgresql_password>`.
1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
username of `praefect` (recommended). The command will request the password `<praefect_postgresql_password>`
and confirmation. Use the value that is output by this command in the next
step as the value of `<praefect_postgresql_password_hash>`:
```shell
sudo gitlab-ctl pg-password-md5 praefect
```
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except PostgreSQL and Consul
roles ['postgres_role']
repmgr['enable'] = false
patroni['enable'] = false
# PostgreSQL configuration
postgresql['listen_address'] = '0.0.0.0'
postgresql['max_connections'] = 200
gitlab_rails['auto_migrate'] = false
[Gitaly](../gitaly/index.md) server node requirements are dependent on data, # Configure the Consul agent
specifically the number of projects and those projects' sizes. It's recommended consul['enable'] = true
that a Gitaly server node stores no more than 5 TB of data. Depending on your ## Enable service discovery for Prometheus
repository storage requirements, you may require additional Gitaly server nodes. consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = "<praefect_postgresql_password_hash>"
# Replace XXX.XXX.XXX.XXX/YY with Network Address
postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
postgres_exporter['listen_address'] = '0.0.0.0:9187'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Follow the [post configuration](#praefect-postgresql-post-configuration).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
#### Praefect HA PostgreSQL third-party solution
[As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
Praefect's database is recommended if aiming for full High Availability.
There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
- A static IP for all connections that doesn't change on failover.
- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
#### Praefect PostgreSQL post-configuration
After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
The password for the user is the same as the one you configured earlier as `<praefect_postgresql_password>`.
This is how this would work with a Omnibus GitLab PostgreSQL setup:
1. SSH in to the Praefect PostgreSQL node.
1. Connect to the PostgreSQL server with administrative access.
The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
The database `template1` is used because it is created by default on all PostgreSQL servers.
```shell
/opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
```shell
/opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create a new database `praefect_production`:
```shell
CREATE DATABASE praefect_production WITH ENCODING=UTF8;
```
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
### Configure Praefect
Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
it. This section details how to configure it.
Praefect requires several secret tokens to secure communications across the Cluster:
- `<praefect_external_token>`: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
- `<praefect_internal_token>`: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
- `<praefect_postgresql_password>`: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
the details of each Gitaly node that makes up the cluster. Each storage is also given a name
and this name is used in several areas of the config. In this guide, the name of the storage will be
`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
to use Gitaly Cluster, you may need to use a different name.
Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
The following IPs will be used as an example:
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
To configure the Praefect nodes, on each one:
1. SSH in to the Praefect server.
1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
```ruby
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
puma['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
# Praefect Configuration
praefect['enable'] = true
praefect['listen_addr'] = '0.0.0.0:2305'
gitlab_rails['rake_cache_clear'] = false
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Praefect External Token
# This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
praefect['auth_token'] = '<praefect_external_token>'
# Praefect Database Settings
praefect['database_host'] = '10.6.0.141'
praefect['database_port'] = 5432
# `no_proxy` settings must always be a direct connection for caching
praefect['database_host_no_proxy'] = '10.6.0.141'
praefect['database_port_no_proxy'] = 5432
praefect['database_dbname'] = 'praefect_production'
praefect['database_user'] = 'praefect'
praefect['database_password'] = '<praefect_postgresql_password>'
# Praefect Virtual Storage config
# Name of storage hash must match storage name in git_data_dirs on GitLab
# server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
praefect['virtual_storages'] = {
'default' => {
'gitaly-1' => {
'address' => 'tcp://10.6.0.91:8075',
'token' => '<praefect_internal_token>',
'primary' => true
},
'gitaly-2' => {
'address' => 'tcp://10.6.0.92:8075',
'token' => '<praefect_internal_token>'
},
'gitaly-3' => {
'address' => 'tcp://10.6.0.93:8075',
'token' => '<praefect_internal_token>'
},
}
}
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Configure Gitaly
The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
requirements that are dependent on data, specifically the number of projects
and those projects' sizes. It's recommended that a Gitaly Cluster stores
no more than 5 TB of data on each node. Depending on your
repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
...@@ -1325,36 +1631,21 @@ adjusted to greater or lesser values depending on the scale of your ...@@ -1325,36 +1631,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider, environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly. refer to their documentation about how to configure IOPS correctly.
Be sure to note the following items: Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-cluster-tls-support).
- The GitLab Rails application shards repositories into For configuring Gitaly you should note the following:
[repository storage paths](../repository_storage_paths.md).
- A Gitaly server can host one or more storage paths.
- A GitLab server can use one or more Gitaly server nodes.
- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
clients.
- Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-tls-support).
NOTE: - `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
The token referred to throughout the Gitaly documentation is an arbitrary - `auth_token` should be the same as `praefect_internal_token`
password selected by the administrator. This token is unrelated to tokens
created for the GitLab API or other similar web API tokens.
This section describes how to configure two Gitaly servers, with the following
IPs and domain names:
- `10.6.0.91`: Gitaly 1 (`gitaly1.internal`)
- `10.6.0.92`: Gitaly 2 (`gitaly2.internal`)
Assumptions about your servers include having the secret token be `gitalysecret`, The following IPs will be used as an example:
and that your GitLab installation has three repository storages:
- `default` on Gitaly 1 - `10.6.0.91`: Gitaly 1
- `storage1` on Gitaly 1 - `10.6.0.92`: Gitaly 2
- `storage2` on Gitaly 2 - `10.6.0.93`: Gitaly 3
On each node: On each node:
...@@ -1364,21 +1655,9 @@ On each node: ...@@ -1364,21 +1655,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure 1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token: storage paths, enable the network listener, and to configure the token:
<!--
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```ruby
# /etc/gitlab/gitlab.rb # /etc/gitlab/gitlab.rb
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
# The following two values must be the same as their respective values
# of the GitLab Rails application setup
gitaly['auth_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
# Avoid running unnecessary services on the Gitaly server # Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false postgresql['enable'] = false
redis['enable'] = false redis['enable'] = false
...@@ -1407,36 +1686,42 @@ On each node: ...@@ -1407,36 +1686,42 @@ On each node:
# firewalls to restrict access to this address/port. # firewalls to restrict access to this address/port.
# Comment out following line if you only want to support TLS connections # Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075" gitaly['listen_addr'] = "0.0.0.0:8075"
# Gitaly Auth Token
# Should be the same as praefect_internal_token
gitaly['auth_token'] = '<praefect_internal_token>'
``` ```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- On `gitaly1.internal`: - On Gitaly node 1:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { "gitaly-1" => {
'path' => '/var/opt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
'storage1' => {
'path' => '/mnt/gitlab/git-data'
},
}) })
``` ```
- On `gitaly2.internal`: - On Gitaly node 2:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'storage2' => { "gitaly-2" => {
'path' => '/mnt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
}) })
``` ```
<!-- - On Gitaly node 3:
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab ```ruby
--> git_data_dirs({
"gitaly-3" => {
"path" => "/var/opt/gitlab/git-data"
}
})
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on then replace the file of the same name on this server. If that file isn't on
...@@ -1444,34 +1729,44 @@ On each node: ...@@ -1444,34 +1729,44 @@ On each node:
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Gitaly TLS support ### Gitaly Cluster TLS support
Gitaly supports TLS encryption. To be able to communicate Praefect supports TLS encryption. To communicate with a Praefect instance that listens
with a Gitaly instance that listens for secure connections you will need to use `tls://` URL for secure connections, you must:
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
You will need to bring your own certificates as this isn't provided automatically. - Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
The certificate, or its certificate authority, must be installed on all Gitaly in the GitLab configuration.
nodes (including the Gitaly node using the certificate) and on all client nodes - Bring your own certificates because this isn't provided automatically. The certificate
that communicate with it following the procedure described in corresponding to each Praefect server must be installed on that Praefect server.
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
The self-signed certificate must specify the address you use to access the and on all Praefect clients that communicate with it following the procedure described in
Gitaly server. If you are addressing the Gitaly server by a hostname, you can [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
either use the Common Name field for this, or add it as a Subject Alternative
Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
It's possible to configure Gitaly servers with both an unencrypted listening Note the following:
address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
at the same time. This allows you to do a gradual transition from unencrypted to
encrypted traffic, if necessary.
To configure Gitaly with TLS: - The certificate must specify the address you use to access the Praefect server. If
addressing the Praefect server by:
1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: - Hostname, you can either use the Common Name field for this, or add it as a Subject
Alternative Name.
- IP address, you must add it as a Subject Alternative Name to the certificate.
- You can configure Praefect servers with both an unencrypted listening address
`listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
This allows you to do a gradual transition from unencrypted to encrypted traffic, if
necessary.
- The Internal Load Balancer will also access to the certificates and need to be configured
to allow for TLS passthrough.
Refer to the load balancers documentation on how to configure this.
To configure Praefect with TLS:
1. Create certificates for Praefect servers.
1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
and certificate there:
```shell ```shell
sudo mkdir -p /etc/gitlab/ssl sudo mkdir -p /etc/gitlab/ssl
...@@ -1480,27 +1775,34 @@ To configure Gitaly with TLS: ...@@ -1480,27 +1775,34 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem sudo chmod 644 key.pem cert.pem
``` ```
1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when 1. Edit `/etc/gitlab/gitlab.rb` and add:
calling into itself:
```shell ```ruby
sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/ praefect['tls_listen_addr'] = "0.0.0.0:3305"
praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Edit `/etc/gitlab/gitlab.rb` and add: 1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<!-- 1. On the Praefect clients (including each Gitaly server), copy the certificates,
updates to following example must also be made at or their certificate authority, into `/etc/gitlab/trusted-certs`:
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```shell
gitaly['tls_listen_addr'] = "0.0.0.0:9999" sudo cp cert.pem /etc/gitlab/trusted-certs/
gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Delete `gitaly['listen_addr']` to allow only encrypted connections. 1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
`/etc/gitlab/gitlab.rb` as follows:
```ruby
git_data_dirs({
"default" => {
"gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
"gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
}
})
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
...@@ -1587,12 +1889,15 @@ To configure the Sidekiq nodes, on each one: ...@@ -1587,12 +1889,15 @@ To configure the Sidekiq nodes, on each one:
### Gitaly ### ### Gitaly ###
####################################### #######################################
# git_data_dirs get configured for the Praefect virtual storage
# Address is Internal Load Balancer for Praefect
# Token is praefect_external_token
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
####################################### #######################################
### Postgres ### ### Postgres ###
...@@ -1624,7 +1929,7 @@ To configure the Sidekiq nodes, on each one: ...@@ -1624,7 +1929,7 @@ To configure the Sidekiq nodes, on each one:
node_exporter['listen_address'] = '0.0.0.0:9100' node_exporter['listen_address'] = '0.0.0.0:9100'
# Rails Status for prometheus # Rails Status for prometheus
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -1671,6 +1976,8 @@ The following IPs will be used as an example: ...@@ -1671,6 +1976,8 @@ The following IPs will be used as an example:
- `10.6.0.111`: GitLab application 1 - `10.6.0.111`: GitLab application 1
- `10.6.0.112`: GitLab application 2 - `10.6.0.112`: GitLab application 2
- `10.6.0.113`: GitLab application 3 - `10.6.0.113`: GitLab application 3
- `10.6.0.114`: GitLab application 4
- `10.6.0.115`: GitLab application 5
On each node perform the following: On each node perform the following:
...@@ -1690,17 +1997,14 @@ On each node perform the following: ...@@ -1690,17 +1997,14 @@ On each node perform the following:
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests # git_data_dirs get configured for the Praefect virtual storage
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API. # Address is Internal Load Balancer for Praefect
# The following two values must be the same as their respective values # Token is praefect_external_token
# of the Gitaly setup
gitlab_rails['gitaly_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
## Disable components that will not be on the GitLab application server ## Disable components that will not be on the GitLab application server
...@@ -1755,8 +2059,8 @@ On each node perform the following: ...@@ -1755,8 +2059,8 @@ On each node perform the following:
# Add the monitoring node's IP address to the monitoring whitelist and allow it to # Add the monitoring node's IP address to the monitoring whitelist and allow it to
# scrape the NGINX metrics # scrape the NGINX metrics
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -1779,14 +2083,15 @@ On each node perform the following: ...@@ -1779,14 +2083,15 @@ On each node perform the following:
``` ```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the 1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`: `git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "default" => {
'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
``` ```
...@@ -1891,7 +2196,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and ...@@ -1891,7 +2196,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and
The following IP will be used as an example: The following IP will be used as an example:
- `10.6.0.121`: Prometheus - `10.6.0.151`: Prometheus
To configure the Monitoring node: To configure the Monitoring node:
......
...@@ -20,78 +20,107 @@ For a full list of reference architectures, see ...@@ -20,78 +20,107 @@ For a full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 3,000 > - **Supported users (approximate):** 3,000
> - **High Availability:** Yes > - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS > - **Test requests per second (RPS) rates:** API: 60 RPS, Web: 6 RPS, Git (Pull): 6 RPS, Git (Push): 1 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-----------------------|----------------|-------------|---------| |--------------------------------------------|-------------|-----------------------|----------------|-------------|---------|
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
| Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | PostgreSQL | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Gitaly | 2 (minimum) | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Gitaly | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | Praefect | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| GitLab Rails | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | `c5.2xlarge` | F8s v2 | | Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
| GitLab Rails | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a | | Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
```mermaid ```plantuml
stateDiagram-v2 @startuml 3k
[*] --> LoadBalancer card "**External Load Balancer**" as elb #6a9be7
LoadBalancer --> ApplicationServer card "**Internal Load Balancer**" as ilb #9370DB
ApplicationServer --> BackgroundJobs together {
ApplicationServer --> Gitaly collections "**GitLab Rails** x3" as gitlab #32CD32
ApplicationServer --> Redis collections "**Sidekiq** x4" as sidekiq #ff8dd1
ApplicationServer --> PgBouncer }
PgBouncer --> Database
ApplicationServer --> ObjectStorage together {
BackgroundJobs --> ObjectStorage card "**Prometheus + Grafana**" as monitor #7FFFD4
collections "**Consul** x3" as consul #e76a9b
ApplicationMonitoring -->ApplicationServer }
ApplicationMonitoring -->Redis
ApplicationMonitoring -->PgBouncer card "Gitaly Cluster" as gitaly_cluster {
ApplicationMonitoring -->Database collections "**Praefect** x3" as praefect #FF8C00
ApplicationMonitoring -->BackgroundJobs collections "**Gitaly** x3" as gitaly #FF8C00
card "**Praefect PostgreSQL***\n//Non fault-tolerant//" as praefect_postgres #FF8C00
state Database {
"PG_Primary_Node" praefect -[#FF8C00]-> gitaly
"PG_Secondary_Node_1..2" praefect -[#FF8C00]> praefect_postgres
} }
state Redis {
"R_Primary_Node" card "Database" as database {
"R_Replica_Node_1..2" collections "**PGBouncer** x3" as pgbouncer #4EA7FF
"R_Consul/Sentinel_1..3" card "**PostgreSQL** (Primary)" as postgres_primary #4EA7FF
} collections "**PostgreSQL** (Secondary) x2" as postgres_secondary #4EA7FF
state Gitaly { pgbouncer -[#4EA7FF]-> postgres_primary
"Gitaly_1..2" postgres_primary .[#4EA7FF]> postgres_secondary
} }
state BackgroundJobs { card "redis" as redis {
"Sidekiq_1..4" collections "**Redis Persistent** x3" as redis_persistent #FF6347
} collections "**Redis Cache** x3" as redis_cache #FF6347
collections "**Redis Persistent Sentinel** x3" as redis_persistent_sentinel #FF6347
state ApplicationServer { collections "**Redis Cache Sentinel** x3"as redis_cache_sentinel #FF6347
"GitLab_Rails_1..3"
} redis_persistent <.[#FF6347]- redis_persistent_sentinel
redis_cache <.[#FF6347]- redis_cache_sentinel
state LoadBalancer { }
"LoadBalancer_1"
} cloud "**Object Storage**" as object_storage #white
state ApplicationMonitoring { elb -[#6a9be7]-> gitlab
"Prometheus" elb -[#6a9be7]--> monitor
"Grafana"
} gitlab -[#32CD32]> sidekiq
gitlab -[#32CD32]--> ilb
state PgBouncer { gitlab -[#32CD32]-> object_storage
"Internal_Load_Balancer" gitlab -[#32CD32]---> redis
"PgBouncer_1..3" gitlab -[hidden]-> monitor
} gitlab -[hidden]-> consul
sidekiq -[#ff8dd1]--> ilb
sidekiq -[#ff8dd1]-> object_storage
sidekiq -[#ff8dd1]---> redis
sidekiq -[hidden]-> monitor
sidekiq -[hidden]-> consul
ilb -[#9370DB]-> gitaly_cluster
ilb -[#9370DB]-> database
consul .[#e76a9b]u-> gitlab
consul .[#e76a9b]u-> sidekiq
consul .[#e76a9b]> monitor
consul .[#e76a9b]-> database
consul .[#e76a9b]-> gitaly_cluster
consul .[#e76a9b,norank]--> redis
monitor .[#7FFFD4]u-> gitlab
monitor .[#7FFFD4]u-> sidekiq
monitor .[#7FFFD4]> consul
monitor .[#7FFFD4]-> database
monitor .[#7FFFD4]-> gitaly_cluster
monitor .[#7FFFD4,norank]--> redis
monitor .[#7FFFD4]> ilb
monitor .[#7FFFD4,norank]u--> elb
@enduml
``` ```
The Google Cloud Platform (GCP) architectures were built and tested using the The Google Cloud Platform (GCP) architectures were built and tested using the
...@@ -106,19 +135,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object- ...@@ -106,19 +135,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object-
is recommended instead of using NFS. Using an object storage service also is recommended instead of using NFS. Using an object storage service also
doesn't require you to provision and maintain a node. doesn't require you to provision and maintain a node.
It's also worth noting that at this time [Praefect requires its own database server](../gitaly/praefect.md#postgresql) and
that to achieve full High Availability a third party PostgreSQL database solution will be required.
We hope to offer a built in solutions for these restrictions in the future but in the meantime a non HA PostgreSQL server
can be set up via Omnibus GitLab, which the above specs reflect. Refer to the following issues for more information: [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919) & [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398)
## Setup components ## Setup components
To set up GitLab and its components to accommodate up to 3,000 users: To set up GitLab and its components to accommodate up to 3,000 users:
1. [Configure the external load balancing node](#configure-the-external-load-balancer) 1. [Configure the external load balancer](#configure-the-external-load-balancer)
to handle the load balancing of the GitLab application services nodes. to handle the load balancing of the GitLab application services nodes.
1. [Configure the internal load balancer](#configure-the-internal-load-balancer).
to handle the load balancing of GitLab application internal connections.
1. [Configure Redis](#configure-redis). 1. [Configure Redis](#configure-redis).
1. [Configure Consul and Sentinel](#configure-consul-and-sentinel). 1. [Configure Consul and Sentinel](#configure-consul-and-sentinel).
1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab. 1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab.
1. [Configure PgBouncer](#configure-pgbouncer). 1. [Configure PgBouncer](#configure-pgbouncer).
1. [Configure the internal load balancing node](#configure-the-internal-load-balancer). 1. [Configure Gitaly Cluster](#configure-gitaly-cluster),
1. [Configure Gitaly](#configure-gitaly), provides access to the Git repositories.
which provides access to the Git repositories.
1. [Configure Sidekiq](#configure-sidekiq). 1. [Configure Sidekiq](#configure-sidekiq).
1. [Configure the main GitLab Rails application](#configure-gitlab-rails) 1. [Configure the main GitLab Rails application](#configure-gitlab-rails)
to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend
...@@ -155,6 +190,11 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -155,6 +190,11 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.20`: Internal Load Balancer - `10.6.0.20`: Internal Load Balancer
- `10.6.0.51`: Gitaly 1 - `10.6.0.51`: Gitaly 1
- `10.6.0.52`: Gitaly 2 - `10.6.0.52`: Gitaly 2
- `10.6.0.93`: Gitaly 3
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
- `10.6.0.141`: Praefect PostgreSQL 1 (non HA)
- `10.6.0.71`: Sidekiq 1 - `10.6.0.71`: Sidekiq 1
- `10.6.0.72`: Sidekiq 2 - `10.6.0.72`: Sidekiq 2
- `10.6.0.73`: Sidekiq 3 - `10.6.0.73`: Sidekiq 3
...@@ -285,6 +325,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ...@@ -285,6 +325,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
</a> </a>
</div> </div>
## Configure the internal load balancer
The Internal Load Balancer is used to balance any internal connections the GitLab environment requires
such as connections to [PgBouncer](#configure-pgbouncer) and [Praefect](#configure-praefect) (Gitaly Cluster).
Note that it's a separate node from the External Load Balancer and shouldn't have any access externally.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
frontend internal-praefect-tcp-in
bind *:2305
mode tcp
option tcplog
option clitcpka
default_backend praefect
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
backend praefect
mode tcp
option tcp-check
option srvtcpka
server praefect1 10.6.0.131:2305 check
server praefect2 10.6.0.132:2305 check
server praefect3 10.6.0.133:2305 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Redis ## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica** Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
...@@ -925,45 +1030,96 @@ The following IPs will be used as an example: ...@@ -925,45 +1030,96 @@ The following IPs will be used as an example:
</a> </a>
</div> </div>
### Configure the internal load balancer ## Configure Gitaly Cluster
If you're running more than one PgBouncer node as recommended, then at this time you'll need to set [Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
up a TCP internal load balancer to serve each correctly. In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
The following IP will be used as an example: The recommended cluster setup includes the following components:
- `10.6.0.20`: Internal Load Balancer - 3 Gitaly nodes: Replicated storage of Git repositories.
- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
is required for Praefect database connections to be made highly available.
- 1 load balancer: A load balancer is required for Praefect. The
[internal load balancer](#configure-the-internal-load-balancer) will be used.
Here's how you could do it with [HAProxy](https://www.haproxy.org/): This section will detail how to configure the recommended standard setup in order.
For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
```plaintext ### Configure Praefect PostgreSQL
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
bind *:6432 A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
mode tcp
option tcplog
default_backend pgbouncer #### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
backend pgbouncer The following IPs will be used as an example:
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check - `10.6.0.141`: Praefect PostgreSQL
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
```
Refer to your preferred Load Balancer's documentation for further guidance. First, make sure to [install](https://about.gitlab.com/install/)
the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
install the necessary dependencies from step 1, and add the
GitLab package repository from step 2. When installing GitLab
in the second step, do not supply the `EXTERNAL_URL` value.
1. SSH in to the Praefect PostgreSQL node.
1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as `<praefect_postgresql_password>`.
1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
username of `praefect` (recommended). The command will request the password `<praefect_postgresql_password>`
and confirmation. Use the value that is output by this command in the next
step as the value of `<praefect_postgresql_password_hash>`:
```shell
sudo gitlab-ctl pg-password-md5 praefect
```
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except PostgreSQL and Consul
roles ['postgres_role']
repmgr['enable'] = false
patroni['enable'] = false
# PostgreSQL configuration
postgresql['listen_address'] = '0.0.0.0'
postgresql['max_connections'] = 200
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = "<praefect_postgresql_password_hash>"
# Replace XXX.XXX.XXX.XXX/YY with Network Address
postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
postgres_exporter['listen_address'] = '0.0.0.0:9187'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Follow the [post configuration](#praefect-postgresql-post-configuration).
<div align="right"> <div align="right">
<a type="button" class="btn btn-default" href="#setup-components"> <a type="button" class="btn btn-default" href="#setup-components">
...@@ -971,19 +1127,186 @@ Refer to your preferred Load Balancer's documentation for further guidance. ...@@ -971,19 +1127,186 @@ Refer to your preferred Load Balancer's documentation for further guidance.
</a> </a>
</div> </div>
## Configure Gitaly #### Praefect HA PostgreSQL third-party solution
NOTE: [As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
[Gitaly Cluster](../gitaly/praefect.md) support Praefect's database is recommended if aiming for full High Availability.
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
some Architecture specs will likely change as a result to support the new
and improved designed. - A static IP for all connections that doesn't change on failover.
- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
#### Praefect PostgreSQL post-configuration
After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
The password for the user is the same as the one you configured earlier as `<praefect_postgresql_password>`.
This is how this would work with a Omnibus GitLab PostgreSQL setup:
1. SSH in to the Praefect PostgreSQL node.
1. Connect to the PostgreSQL server with administrative access.
The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
The database `template1` is used because it is created by default on all PostgreSQL servers.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data, ```shell
specifically the number of projects and those projects' sizes. It's recommended /opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
that a Gitaly server node stores no more than 5 TB of data. Depending on your ```
repository storage requirements, you may require additional Gitaly server nodes.
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
```shell
/opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create a new database `praefect_production`:
```shell
CREATE DATABASE praefect_production WITH ENCODING=UTF8;
```
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
### Configure Praefect
Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
it. This section details how to configure it.
Praefect requires several secret tokens to secure communications across the Cluster:
- `<praefect_external_token>`: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
- `<praefect_internal_token>`: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
- `<praefect_postgresql_password>`: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
the details of each Gitaly node that makes up the cluster. Each storage is also given a name
and this name is used in several areas of the config. In this guide, the name of the storage will be
`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
to use Gitaly Cluster, you may need to use a different name.
Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
The following IPs will be used as an example:
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
To configure the Praefect nodes, on each one:
1. SSH in to the Praefect server.
1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
```ruby
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
puma['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
# Praefect Configuration
praefect['enable'] = true
praefect['listen_addr'] = '0.0.0.0:2305'
gitlab_rails['rake_cache_clear'] = false
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Praefect External Token
# This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
praefect['auth_token'] = '<praefect_external_token>'
# Praefect Database Settings
praefect['database_host'] = '10.6.0.141'
praefect['database_port'] = 5432
# `no_proxy` settings must always be a direct connection for caching
praefect['database_host_no_proxy'] = '10.6.0.141'
praefect['database_port_no_proxy'] = 5432
praefect['database_dbname'] = 'praefect_production'
praefect['database_user'] = 'praefect'
praefect['database_password'] = '<praefect_postgresql_password>'
# Praefect Virtual Storage config
# Name of storage hash must match storage name in git_data_dirs on GitLab
# server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
praefect['virtual_storages'] = {
'default' => {
'gitaly-1' => {
'address' => 'tcp://10.6.0.91:8075',
'token' => '<praefect_internal_token>',
'primary' => true
},
'gitaly-2' => {
'address' => 'tcp://10.6.0.92:8075',
'token' => '<praefect_internal_token>'
},
'gitaly-3' => {
'address' => 'tcp://10.6.0.93:8075',
'token' => '<praefect_internal_token>'
},
}
}
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Configure Gitaly
The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
requirements that are dependent on data, specifically the number of projects
and those projects' sizes. It's recommended that a Gitaly Cluster stores
no more than 5 TB of data on each node. Depending on your
repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
...@@ -994,36 +1317,21 @@ adjusted to greater or lesser values depending on the scale of your ...@@ -994,36 +1317,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider, environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly. refer to their documentation about how to configure IOPS correctly.
Be sure to note the following items: Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-cluster-tls-support).
- The GitLab Rails application shards repositories into For configuring Gitaly you should note the following:
[repository storage paths](../repository_storage_paths.md).
- A Gitaly server can host one or more storage paths.
- A GitLab server can use one or more Gitaly server nodes.
- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
clients.
- Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-tls-support).
NOTE:
The token referred to throughout the Gitaly documentation is an arbitrary
password selected by the administrator. This token is unrelated to tokens
created for the GitLab API or other similar web API tokens.
This section describes how to configure two Gitaly servers, with the following - `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
IPs and domain names: - `auth_token` should be the same as `praefect_internal_token`
- `10.6.0.51`: Gitaly 1 (`gitaly1.internal`) The following IPs will be used as an example:
- `10.6.0.52`: Gitaly 2 (`gitaly2.internal`)
Assumptions about your servers include having the secret token be `gitalysecret`,
and that your GitLab installation has three repository storages:
- `default` on Gitaly 1 - `10.6.0.91`: Gitaly 1
- `storage1` on Gitaly 1 - `10.6.0.92`: Gitaly 2
- `storage2` on Gitaly 2 - `10.6.0.93`: Gitaly 3
On each node: On each node:
...@@ -1033,21 +1341,9 @@ On each node: ...@@ -1033,21 +1341,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure 1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token: storage paths, enable the network listener, and to configure the token:
<!--
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```ruby
# /etc/gitlab/gitlab.rb # /etc/gitlab/gitlab.rb
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
# The following two values must be the same as their respective values
# of the GitLab Rails application setup
gitaly['auth_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
# Avoid running unnecessary services on the Gitaly server # Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false postgresql['enable'] = false
redis['enable'] = false redis['enable'] = false
...@@ -1057,7 +1353,6 @@ On each node: ...@@ -1057,7 +1353,6 @@ On each node:
sidekiq['enable'] = false sidekiq['enable'] = false
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['enable'] = false grafana['enable'] = false
gitlab_exporter['enable'] = false
# If you run a separate monitoring node you can disable these services # If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false alertmanager['enable'] = false
...@@ -1078,101 +1373,86 @@ On each node: ...@@ -1078,101 +1373,86 @@ On each node:
# Comment out following line if you only want to support TLS connections # Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075" gitaly['listen_addr'] = "0.0.0.0:8075"
## Enable service discovery for Prometheus # Gitaly Auth Token
consul['enable'] = true # Should be the same as praefect_internal_token
consul['monitoring_service_discovery'] = true gitaly['auth_token'] = '<praefect_internal_token>'
# Set the network addresses that the exporters will listen on for monitoring
gitaly['prometheus_listen_addr'] = "0.0.0.0:9236"
node_exporter['listen_address'] = '0.0.0.0:9100'
gitlab_rails['prometheus_address'] = '10.6.0.81:9090'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
``` ```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- On `gitaly1.internal`: - On Gitaly node 1:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { "gitaly-1" => {
'path' => '/var/opt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
'storage1' => {
'path' => '/mnt/gitlab/git-data'
},
}) })
``` ```
- On `gitaly2.internal`: - On Gitaly node 2:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'storage2' => { "gitaly-2" => {
'path' => '/mnt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
}) })
``` ```
<!-- - On Gitaly node 3:
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab ```ruby
--> git_data_dirs({
"gitaly-3" => {
"path" => "/var/opt/gitlab/git-data"
}
})
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Confirm that Gitaly can perform callbacks to the internal API:
```shell ### Gitaly Cluster TLS support
sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml
```
1. Verify the GitLab services are running: Praefect supports TLS encryption. To communicate with a Praefect instance that listens
for secure connections, you must:
```shell - Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
sudo gitlab-ctl status in the GitLab configuration.
``` - Bring your own certificates because this isn't provided automatically. The certificate
corresponding to each Praefect server must be installed on that Praefect server.
The output should be similar to the following: Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
and on all Praefect clients that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
```plaintext Note the following:
run: consul: (pid 30339) 77006s; run: log: (pid 29878) 77020s
run: gitaly: (pid 30351) 77005s; run: log: (pid 29660) 77040s
run: logrotate: (pid 7760) 3213s; run: log: (pid 29782) 77032s
run: node-exporter: (pid 30378) 77004s; run: log: (pid 29812) 77026s
```
### Gitaly TLS support - The certificate must specify the address you use to access the Praefect server. If
addressing the Praefect server by:
Gitaly supports TLS encryption. To be able to communicate - Hostname, you can either use the Common Name field for this, or add it as a Subject
with a Gitaly instance that listens for secure connections you will need to use `tls://` URL Alternative Name.
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration. - IP address, you must add it as a Subject Alternative Name to the certificate.
You will need to bring your own certificates as this isn't provided automatically. - You can configure Praefect servers with both an unencrypted listening address
The certificate, or its certificate authority, must be installed on all Gitaly `listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
nodes (including the Gitaly node using the certificate) and on all client nodes This allows you to do a gradual transition from unencrypted to encrypted traffic, if
that communicate with it following the procedure described in necessary.
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: - The Internal Load Balancer will also access to the certificates and need to be configured
The self-signed certificate must specify the address you use to access the to allow for TLS passthrough.
Gitaly server. If you are addressing the Gitaly server by a hostname, you can Refer to the load balancers documentation on how to configure this.
either use the Common Name field for this, or add it as a Subject Alternative
Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
It's possible to configure Gitaly servers with both an unencrypted listening To configure Praefect with TLS:
address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
at the same time. This allows you to do a gradual transition from unencrypted to
encrypted traffic, if necessary.
To configure Gitaly with TLS: 1. Create certificates for Praefect servers.
1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: 1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
and certificate there:
```shell ```shell
sudo mkdir -p /etc/gitlab/ssl sudo mkdir -p /etc/gitlab/ssl
...@@ -1181,27 +1461,35 @@ To configure Gitaly with TLS: ...@@ -1181,27 +1461,35 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem sudo chmod 644 key.pem cert.pem
``` ```
1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when 1. Edit `/etc/gitlab/gitlab.rb` and add:
calling into itself:
```shell ```ruby
sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/ praefect['tls_listen_addr'] = "0.0.0.0:3305"
praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Edit `/etc/gitlab/gitlab.rb` and add: 1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<!-- 1. On the Praefect clients (including each Gitaly server), copy the certificates,
updates to following example must also be made at or their certificate authority, into `/etc/gitlab/trusted-certs`:
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
--> ```shell
sudo cp cert.pem /etc/gitlab/trusted-certs/
```
1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
`/etc/gitlab/gitlab.rb` as follows:
```ruby ```ruby
gitaly['tls_listen_addr'] = "0.0.0.0:9999" git_data_dirs({
gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem" "default" => {
gitaly['key_path'] = "/etc/gitlab/ssl/key.pem" "gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
"gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
}
})
``` ```
1. Delete `gitaly['listen_addr']` to allow only encrypted connections.
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<div align="right"> <div align="right">
...@@ -1272,17 +1560,20 @@ To configure the Sidekiq nodes, one each one: ...@@ -1272,17 +1560,20 @@ To configure the Sidekiq nodes, one each one:
### Gitaly ### ### Gitaly ###
####################################### #######################################
# git_data_dirs get configured for the Praefect virtual storage
# Address is Internal Load Balancer for Praefect
# Token is praefect_external_token
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
####################################### #######################################
### Postgres ### ### Postgres ###
####################################### #######################################
gitlab_rails['db_host'] = '10.6.0.20' # internal load balancer IP gitlab_rails['db_host'] = '10.6.0.40' # internal load balancer IP
gitlab_rails['db_port'] = 6432 gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = '<postgresql_user_password>' gitlab_rails['db_password'] = '<postgresql_user_password>'
gitlab_rails['db_adapter'] = 'postgresql' gitlab_rails['db_adapter'] = 'postgresql'
...@@ -1401,17 +1692,14 @@ On each node perform the following: ...@@ -1401,17 +1692,14 @@ On each node perform the following:
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests # git_data_dirs get configured for the Praefect virtual storage
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API. # Address is Interal Load Balancer for Praefect
# The following two values must be the same as their respective values # Token is praefect_external_token
# of the Gitaly setup
gitlab_rails['gitaly_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
## Disable components that will not be on the GitLab application server ## Disable components that will not be on the GitLab application server
...@@ -1499,14 +1787,15 @@ On each node perform the following: ...@@ -1499,14 +1787,15 @@ On each node perform the following:
gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "<gcp-terraform-state-bucket-name>" gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "<gcp-terraform-state-bucket-name>"
``` ```
1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the 1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`: `git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "default" => {
'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
``` ```
......
...@@ -12,100 +12,110 @@ full list of reference architectures, see ...@@ -12,100 +12,110 @@ full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 50,000 > - **Supported users (approximate):** 50,000
> - **High Availability:** Yes > - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS > - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git (Pull): 100 RPS, Git (Push): 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------| |-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
| External load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | `c5.2xlarge` | F8s v2 | | External load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 16 vCPU, 60 GB memory | n1-standard-16 | `m5.4xlarge` | D16s v3 | | PostgreSQL | 3 | 32 vCPU, 120 GB memory | n1-standard-32 | m5.8xlarge | D32s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | `c5.2xlarge` | F8s v2 | | Internal load balancing node | 1 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | `t3.small` | B1MS | | Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7 GB memory | g1-small | t3.small | B1MS |
| Gitaly | 2 (minimum) | 64 vCPU, 240 GB memory | n1-standard-64 | `m5.16xlarge` | D64s v3 | | Gitaly | 3 | 64 vCPU, 240 GB memory | n1-standard-64 | m5.16xlarge | D64s v3 |
| Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | `m5.xlarge` | D4s v3 | | Praefect | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| GitLab Rails | 12 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | `c5.9xlarge` | F32s v2 | | Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | Sidekiq | 4 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 12 | 32 vCPU, 28.8 GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a | | Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | NFS server | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
```mermaid ```plantuml
stateDiagram-v2 @startuml 50k
[*] --> LoadBalancer card "**External Load Balancer**" as elb #6a9be7
LoadBalancer --> ApplicationServer card "**Internal Load Balancer**" as ilb #9370DB
ApplicationServer --> BackgroundJobs together {
ApplicationServer --> Gitaly collections "**GitLab Rails** x12" as gitlab #32CD32
ApplicationServer --> Redis_Cache collections "**Sidekiq** x4" as sidekiq #ff8dd1
ApplicationServer --> Redis_Queues }
ApplicationServer --> PgBouncer
PgBouncer --> Database together {
ApplicationServer --> ObjectStorage card "**Prometheus + Grafana**" as monitor #7FFFD4
BackgroundJobs --> ObjectStorage collections "**Consul** x3" as consul #e76a9b
}
ApplicationMonitoring -->ApplicationServer
ApplicationMonitoring -->PgBouncer card "Gitaly Cluster" as gitaly_cluster {
ApplicationMonitoring -->Database collections "**Praefect** x3" as praefect #FF8C00
ApplicationMonitoring -->BackgroundJobs collections "**Gitaly** x3" as gitaly #FF8C00
card "**Praefect PostgreSQL***\n//Non fault-tolerant//" as praefect_postgres #FF8C00
ApplicationServer --> Consul
praefect -[#FF8C00]-> gitaly
Consul --> Database praefect -[#FF8C00]> praefect_postgres
Consul --> PgBouncer }
Redis_Cache --> Consul
Redis_Queues --> Consul card "Database" as database {
BackgroundJobs --> Consul collections "**PGBouncer** x3" as pgbouncer #4EA7FF
card "**PostgreSQL** (Primary)" as postgres_primary #4EA7FF
state Consul { collections "**PostgreSQL** (Secondary) x2" as postgres_secondary #4EA7FF
"Consul_1..3"
} pgbouncer -[#4EA7FF]-> postgres_primary
postgres_primary .[#4EA7FF]> postgres_secondary
state Database { }
"PG_Primary_Node"
"PG_Secondary_Node_1..2" card "redis" as redis {
} collections "**Redis Persistent** x3" as redis_persistent #FF6347
collections "**Redis Cache** x3" as redis_cache #FF6347
state Redis_Cache { collections "**Redis Persistent Sentinel** x3" as redis_persistent_sentinel #FF6347
"R_Cache_Primary_Node" collections "**Redis Cache Sentinel** x3"as redis_cache_sentinel #FF6347
"R_Cache_Replica_Node_1..2"
"R_Cache_Sentinel_1..3" redis_persistent <.[#FF6347]- redis_persistent_sentinel
} redis_cache <.[#FF6347]- redis_cache_sentinel
}
state Redis_Queues {
"R_Queues_Primary_Node" cloud "**Object Storage**" as object_storage #white
"R_Queues_Replica_Node_1..2"
"R_Queues_Sentinel_1..3" elb -[#6a9be7]-> gitlab
} elb -[#6a9be7]--> monitor
state Gitaly { gitlab -[#32CD32]> sidekiq
"Gitaly_1..2" gitlab -[#32CD32]--> ilb
} gitlab -[#32CD32]-> object_storage
gitlab -[#32CD32]---> redis
state BackgroundJobs { gitlab -[hidden]-> monitor
"Sidekiq_1..4" gitlab -[hidden]-> consul
}
sidekiq -[#ff8dd1]--> ilb
state ApplicationServer { sidekiq -[#ff8dd1]-> object_storage
"GitLab_Rails_1..12" sidekiq -[#ff8dd1]---> redis
} sidekiq -[hidden]-> monitor
sidekiq -[hidden]-> consul
state LoadBalancer {
"LoadBalancer_1" ilb -[#9370DB]-> gitaly_cluster
} ilb -[#9370DB]-> database
state ApplicationMonitoring { consul .[#e76a9b]u-> gitlab
"Prometheus" consul .[#e76a9b]u-> sidekiq
"Grafana" consul .[#e76a9b]> monitor
} consul .[#e76a9b]-> database
consul .[#e76a9b]-> gitaly_cluster
state PgBouncer { consul .[#e76a9b,norank]--> redis
"Internal_Load_Balancer"
"PgBouncer_1..3" monitor .[#7FFFD4]u-> gitlab
} monitor .[#7FFFD4]u-> sidekiq
monitor .[#7FFFD4]> consul
monitor .[#7FFFD4]-> database
monitor .[#7FFFD4]-> gitaly_cluster
monitor .[#7FFFD4,norank]--> redis
monitor .[#7FFFD4]> ilb
monitor .[#7FFFD4,norank]u--> elb
@enduml
``` ```
The Google Cloud Platform (GCP) architectures were built and tested using the The Google Cloud Platform (GCP) architectures were built and tested using the
...@@ -120,19 +130,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object- ...@@ -120,19 +130,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object-
is recommended instead of using NFS. Using an object storage service also is recommended instead of using NFS. Using an object storage service also
doesn't require you to provision and maintain a node. doesn't require you to provision and maintain a node.
It's also worth noting that at this time [Praefect requires its own database server](../gitaly/praefect.md#postgresql) and
that to achieve full High Availability a third party PostgreSQL database solution will be required.
We hope to offer a built in solutions for these restrictions in the future but in the meantime a non HA PostgreSQL server
can be set up via Omnibus GitLab, which the above specs reflect. Refer to the following issues for more information: [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919) & [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398)
## Setup components ## Setup components
To set up GitLab and its components to accommodate up to 50,000 users: To set up GitLab and its components to accommodate up to 50,000 users:
1. [Configure the external load balancing node](#configure-the-external-load-balancer) 1. [Configure the external load balancer](#configure-the-external-load-balancer)
to handle the load balancing of the GitLab application services nodes. to handle the load balancing of the GitLab application services nodes.
1. [Configure the internal load balancer](#configure-the-internal-load-balancer).
to handle the loa
1. [Configure Consul](#configure-consul). 1. [Configure Consul](#configure-consul).
1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab. 1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab.
1. [Configure PgBouncer](#configure-pgbouncer). 1. [Configure PgBouncer](#configure-pgbouncer).
1. [Configure the internal load balancing node](#configure-the-internal-load-balancer).
1. [Configure Redis](#configure-redis). 1. [Configure Redis](#configure-redis).
1. [Configure Gitaly](#configure-gitaly), 1. [Configure Gitaly Cluster](#configure-gitaly-cluster),
which provides access to the Git repositories. provides access to the Git repositories.
1. [Configure Sidekiq](#configure-sidekiq). 1. [Configure Sidekiq](#configure-sidekiq).
1. [Configure the main GitLab Rails application](#configure-gitlab-rails) 1. [Configure the main GitLab Rails application](#configure-gitlab-rails)
to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend
...@@ -178,6 +194,11 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -178,6 +194,11 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.83`: Sentinel - Queues 3 - `10.6.0.83`: Sentinel - Queues 3
- `10.6.0.91`: Gitaly 1 - `10.6.0.91`: Gitaly 1
- `10.6.0.92`: Gitaly 2 - `10.6.0.92`: Gitaly 2
- `10.6.0.93`: Gitaly 3
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
- `10.6.0.141`: Praefect PostgreSQL 1 (non HA)
- `10.6.0.101`: Sidekiq 1 - `10.6.0.101`: Sidekiq 1
- `10.6.0.102`: Sidekiq 2 - `10.6.0.102`: Sidekiq 2
- `10.6.0.103`: Sidekiq 3 - `10.6.0.103`: Sidekiq 3
...@@ -185,7 +206,16 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -185,7 +206,16 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.111`: GitLab application 1 - `10.6.0.111`: GitLab application 1
- `10.6.0.112`: GitLab application 2 - `10.6.0.112`: GitLab application 2
- `10.6.0.113`: GitLab application 3 - `10.6.0.113`: GitLab application 3
- `10.6.0.121`: Prometheus - `10.6.0.114`: GitLab application 4
- `10.6.0.115`: GitLab application 5
- `10.6.0.116`: GitLab application 6
- `10.6.0.117`: GitLab application 7
- `10.6.0.118`: GitLab application 8
- `10.6.0.119`: GitLab application 9
- `10.6.0.120`: GitLab application 10
- `10.6.0.121`: GitLab application 11
- `10.6.0.122`: GitLab application 12
- `10.6.0.151`: Prometheus
## Configure the external load balancer ## Configure the external load balancer
...@@ -308,6 +338,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ...@@ -308,6 +338,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
</a> </a>
</div> </div>
## Configure the internal load balancer
The Internal Load Balancer is used to balance any internal connections the GitLab environment requires
such as connections to [PgBouncer](#configure-pgbouncer) and [Praefect](#configure-praefect) (Gitaly Cluster).
Note that it's a separate node from the External Load Balancer and shouldn't have any access externally.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
frontend internal-praefect-tcp-in
bind *:2305
mode tcp
option tcplog
option clitcpka
default_backend praefect
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
backend praefect
mode tcp
option tcp-check
option srvtcpka
server praefect1 10.6.0.131:2305 check
server praefect2 10.6.0.132:2305 check
server praefect3 10.6.0.133:2305 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Consul ## Configure Consul
The following IPs will be used as an example: The following IPs will be used as an example:
...@@ -662,52 +757,6 @@ The following IPs will be used as an example: ...@@ -662,52 +757,6 @@ The following IPs will be used as an example:
</a> </a>
</div> </div>
### Configure the internal load balancer
If you're running more than one PgBouncer node as recommended, then at this time you'll need to set
up a TCP internal load balancer to serve each correctly.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Redis ## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica** Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
...@@ -1302,19 +1351,283 @@ To configure the Sentinel Queues server: ...@@ -1302,19 +1351,283 @@ To configure the Sentinel Queues server:
</a> </a>
</div> </div>
## Configure Gitaly ## Configure Gitaly Cluster
NOTE: [Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
[Gitaly Cluster](../gitaly/praefect.md) support In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified The recommended cluster setup includes the following components:
some Architecture specs will likely change as a result to support the new
and improved designed. - 3 Gitaly nodes: Replicated storage of Git repositories.
- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
is required for Praefect database connections to be made highly available.
- 1 load balancer: A load balancer is required for Praefect. The
[internal load balancer](#configure-the-internal-load-balancer) will be used.
This section will detail how to configure the recommended standard setup in order.
For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
### Configure Praefect PostgreSQL
Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
#### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
The following IPs will be used as an example:
- `10.6.0.141`: Praefect PostgreSQL
First, make sure to [install](https://about.gitlab.com/install/)
the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
install the necessary dependencies from step 1, and add the
GitLab package repository from step 2. When installing GitLab
in the second step, do not supply the `EXTERNAL_URL` value.
1. SSH in to the Praefect PostgreSQL node.
1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as `<praefect_postgresql_password>`.
1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
username of `praefect` (recommended). The command will request the password `<praefect_postgresql_password>`
and confirmation. Use the value that is output by this command in the next
step as the value of `<praefect_postgresql_password_hash>`:
```shell
sudo gitlab-ctl pg-password-md5 praefect
```
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except PostgreSQL and Consul
roles ['postgres_role']
repmgr['enable'] = false
patroni['enable'] = false
# PostgreSQL configuration
postgresql['listen_address'] = '0.0.0.0'
postgresql['max_connections'] = 200
gitlab_rails['auto_migrate'] = false
[Gitaly](../gitaly/index.md) server node requirements are dependent on data, # Configure the Consul agent
specifically the number of projects and those projects' sizes. It's recommended consul['enable'] = true
that a Gitaly server node stores no more than 5 TB of data. Depending on your ## Enable service discovery for Prometheus
repository storage requirements, you may require additional Gitaly server nodes. consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = "<praefect_postgresql_password_hash>"
# Replace XXX.XXX.XXX.XXX/YY with Network Address
postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
postgres_exporter['listen_address'] = '0.0.0.0:9187'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Follow the [post configuration](#praefect-postgresql-post-configuration).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
#### Praefect HA PostgreSQL third-party solution
[As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
Praefect's database is recommended if aiming for full High Availability.
There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
- A static IP for all connections that doesn't change on failover.
- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
#### Praefect PostgreSQL post-configuration
After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
The password for the user is the same as the one you configured earlier as `<praefect_postgresql_password>`.
This is how this would work with a Omnibus GitLab PostgreSQL setup:
1. SSH in to the Praefect PostgreSQL node.
1. Connect to the PostgreSQL server with administrative access.
The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
The database `template1` is used because it is created by default on all PostgreSQL servers.
```shell
/opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
```shell
/opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create a new database `praefect_production`:
```shell
CREATE DATABASE praefect_production WITH ENCODING=UTF8;
```
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
### Configure Praefect
Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
it. This section details how to configure it.
Praefect requires several secret tokens to secure communications across the Cluster:
- `<praefect_external_token>`: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
- `<praefect_internal_token>`: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
- `<praefect_postgresql_password>`: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
the details of each Gitaly node that makes up the cluster. Each storage is also given a name
and this name is used in several areas of the config. In this guide, the name of the storage will be
`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
to use Gitaly Cluster, you may need to use a different name.
Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
The following IPs will be used as an example:
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
To configure the Praefect nodes, on each one:
1. SSH in to the Praefect server.
1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
```ruby
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
puma['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
# Praefect Configuration
praefect['enable'] = true
praefect['listen_addr'] = '0.0.0.0:2305'
gitlab_rails['rake_cache_clear'] = false
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Praefect External Token
# This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
praefect['auth_token'] = '<praefect_external_token>'
# Praefect Database Settings
praefect['database_host'] = '10.6.0.141'
praefect['database_port'] = 5432
# `no_proxy` settings must always be a direct connection for caching
praefect['database_host_no_proxy'] = '10.6.0.141'
praefect['database_port_no_proxy'] = 5432
praefect['database_dbname'] = 'praefect_production'
praefect['database_user'] = 'praefect'
praefect['database_password'] = '<praefect_postgresql_password>'
# Praefect Virtual Storage config
# Name of storage hash must match storage name in git_data_dirs on GitLab
# server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
praefect['virtual_storages'] = {
'default' => {
'gitaly-1' => {
'address' => 'tcp://10.6.0.91:8075',
'token' => '<praefect_internal_token>',
'primary' => true
},
'gitaly-2' => {
'address' => 'tcp://10.6.0.92:8075',
'token' => '<praefect_internal_token>'
},
'gitaly-3' => {
'address' => 'tcp://10.6.0.93:8075',
'token' => '<praefect_internal_token>'
},
}
}
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Configure Gitaly
The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
requirements that are dependent on data, specifically the number of projects
and those projects' sizes. It's recommended that a Gitaly Cluster stores
no more than 5 TB of data on each node. Depending on your
repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
...@@ -1325,36 +1638,21 @@ adjusted to greater or lesser values depending on the scale of your ...@@ -1325,36 +1638,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider, environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly. refer to their documentation about how to configure IOPS correctly.
Be sure to note the following items: Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-cluster-tls-support).
- The GitLab Rails application shards repositories into For configuring Gitaly you should note the following:
[repository storage paths](../repository_storage_paths.md).
- A Gitaly server can host one or more storage paths.
- A GitLab server can use one or more Gitaly server nodes.
- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
clients.
- Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-tls-support).
NOTE: - `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
The token referred to throughout the Gitaly documentation is an arbitrary - `auth_token` should be the same as `praefect_internal_token`
password selected by the administrator. This token is unrelated to tokens
created for the GitLab API or other similar web API tokens.
This section describes how to configure two Gitaly servers, with the following
IPs and domain names:
- `10.6.0.91`: Gitaly 1 (`gitaly1.internal`)
- `10.6.0.92`: Gitaly 2 (`gitaly2.internal`)
Assumptions about your servers include having the secret token be `gitalysecret`, The following IPs will be used as an example:
and that your GitLab installation has three repository storages:
- `default` on Gitaly 1 - `10.6.0.91`: Gitaly 1
- `storage1` on Gitaly 1 - `10.6.0.92`: Gitaly 2
- `storage2` on Gitaly 2 - `10.6.0.93`: Gitaly 3
On each node: On each node:
...@@ -1364,21 +1662,9 @@ On each node: ...@@ -1364,21 +1662,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure 1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token: storage paths, enable the network listener, and to configure the token:
<!--
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```ruby
# /etc/gitlab/gitlab.rb # /etc/gitlab/gitlab.rb
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
# The following two values must be the same as their respective values
# of the GitLab Rails application setup
gitaly['auth_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
# Avoid running unnecessary services on the Gitaly server # Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false postgresql['enable'] = false
redis['enable'] = false redis['enable'] = false
...@@ -1407,36 +1693,42 @@ On each node: ...@@ -1407,36 +1693,42 @@ On each node:
# firewalls to restrict access to this address/port. # firewalls to restrict access to this address/port.
# Comment out following line if you only want to support TLS connections # Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075" gitaly['listen_addr'] = "0.0.0.0:8075"
# Gitaly Auth Token
# Should be the same as praefect_internal_token
gitaly['auth_token'] = '<praefect_internal_token>'
``` ```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- On `gitaly1.internal`: - On Gitaly node 1:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { "gitaly-1" => {
'path' => '/var/opt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
'storage1' => {
'path' => '/mnt/gitlab/git-data'
},
}) })
``` ```
- On `gitaly2.internal`: - On Gitaly node 2:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'storage2' => { "gitaly-2" => {
'path' => '/mnt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
}) })
``` ```
<!-- - On Gitaly node 3:
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab ```ruby
--> git_data_dirs({
"gitaly-3" => {
"path" => "/var/opt/gitlab/git-data"
}
})
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on then replace the file of the same name on this server. If that file isn't on
...@@ -1444,34 +1736,44 @@ On each node: ...@@ -1444,34 +1736,44 @@ On each node:
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Gitaly TLS support ### Gitaly Cluster TLS support
Gitaly supports TLS encryption. To be able to communicate Praefect supports TLS encryption. To communicate with a Praefect instance that listens
with a Gitaly instance that listens for secure connections you will need to use `tls://` URL for secure connections, you must:
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
You will need to bring your own certificates as this isn't provided automatically. - Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
The certificate, or its certificate authority, must be installed on all Gitaly in the GitLab configuration.
nodes (including the Gitaly node using the certificate) and on all client nodes - Bring your own certificates because this isn't provided automatically. The certificate
that communicate with it following the procedure described in corresponding to each Praefect server must be installed on that Praefect server.
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
The self-signed certificate must specify the address you use to access the and on all Praefect clients that communicate with it following the procedure described in
Gitaly server. If you are addressing the Gitaly server by a hostname, you can [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
either use the Common Name field for this, or add it as a Subject Alternative
Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
It's possible to configure Gitaly servers with both an unencrypted listening Note the following:
address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
at the same time. This allows you to do a gradual transition from unencrypted to
encrypted traffic, if necessary.
To configure Gitaly with TLS: - The certificate must specify the address you use to access the Praefect server. If
addressing the Praefect server by:
1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: - Hostname, you can either use the Common Name field for this, or add it as a Subject
Alternative Name.
- IP address, you must add it as a Subject Alternative Name to the certificate.
- You can configure Praefect servers with both an unencrypted listening address
`listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
This allows you to do a gradual transition from unencrypted to encrypted traffic, if
necessary.
- The Internal Load Balancer will also access to the certificates and need to be configured
to allow for TLS passthrough.
Refer to the load balancers documentation on how to configure this.
To configure Praefect with TLS:
1. Create certificates for Praefect servers.
1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
and certificate there:
```shell ```shell
sudo mkdir -p /etc/gitlab/ssl sudo mkdir -p /etc/gitlab/ssl
...@@ -1480,27 +1782,34 @@ To configure Gitaly with TLS: ...@@ -1480,27 +1782,34 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem sudo chmod 644 key.pem cert.pem
``` ```
1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when 1. Edit `/etc/gitlab/gitlab.rb` and add:
calling into itself:
```shell ```ruby
sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/ praefect['tls_listen_addr'] = "0.0.0.0:3305"
praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Edit `/etc/gitlab/gitlab.rb` and add: 1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<!-- 1. On the Praefect clients (including each Gitaly server), copy the certificates,
updates to following example must also be made at or their certificate authority, into `/etc/gitlab/trusted-certs`:
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```shell
gitaly['tls_listen_addr'] = "0.0.0.0:9999" sudo cp cert.pem /etc/gitlab/trusted-certs/
gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Delete `gitaly['listen_addr']` to allow only encrypted connections. 1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
`/etc/gitlab/gitlab.rb` as follows:
```ruby
git_data_dirs({
"default" => {
"gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
"gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
}
})
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
...@@ -1587,12 +1896,15 @@ To configure the Sidekiq nodes, on each one: ...@@ -1587,12 +1896,15 @@ To configure the Sidekiq nodes, on each one:
### Gitaly ### ### Gitaly ###
####################################### #######################################
# git_data_dirs get configured for the Praefect virtual storage
# Address is Internal Load Balancer for Praefect
# Token is praefect_external_token
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
####################################### #######################################
### Postgres ### ### Postgres ###
...@@ -1624,7 +1936,7 @@ To configure the Sidekiq nodes, on each one: ...@@ -1624,7 +1936,7 @@ To configure the Sidekiq nodes, on each one:
node_exporter['listen_address'] = '0.0.0.0:9100' node_exporter['listen_address'] = '0.0.0.0:9100'
# Rails Status for prometheus # Rails Status for prometheus
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -1671,6 +1983,15 @@ The following IPs will be used as an example: ...@@ -1671,6 +1983,15 @@ The following IPs will be used as an example:
- `10.6.0.111`: GitLab application 1 - `10.6.0.111`: GitLab application 1
- `10.6.0.112`: GitLab application 2 - `10.6.0.112`: GitLab application 2
- `10.6.0.113`: GitLab application 3 - `10.6.0.113`: GitLab application 3
- `10.6.0.114`: GitLab application 4
- `10.6.0.115`: GitLab application 5
- `10.6.0.116`: GitLab application 6
- `10.6.0.117`: GitLab application 7
- `10.6.0.118`: GitLab application 8
- `10.6.0.119`: GitLab application 9
- `10.6.0.120`: GitLab application 10
- `10.6.0.121`: GitLab application 11
- `10.6.0.122`: GitLab application 12
On each node perform the following: On each node perform the following:
...@@ -1690,17 +2011,14 @@ On each node perform the following: ...@@ -1690,17 +2011,14 @@ On each node perform the following:
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests # git_data_dirs get configured for the Praefect virtual storage
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API. # Address is Interal Load Balancer for Praefect
# The following two values must be the same as their respective values # Token is praefect_external_token
# of the Gitaly setup
gitlab_rails['gitaly_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
## Disable components that will not be on the GitLab application server ## Disable components that will not be on the GitLab application server
...@@ -1755,8 +2073,8 @@ On each node perform the following: ...@@ -1755,8 +2073,8 @@ On each node perform the following:
# Add the monitoring node's IP address to the monitoring whitelist and allow it to # Add the monitoring node's IP address to the monitoring whitelist and allow it to
# scrape the NGINX metrics # scrape the NGINX metrics
gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] gitlab_rails['monitoring_whitelist'] = ['10.6.0.151/32', '127.0.0.0/8']
nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.151/32', '127.0.0.0/8']
############################# #############################
### Object storage ### ### Object storage ###
...@@ -1779,14 +2097,15 @@ On each node perform the following: ...@@ -1779,14 +2097,15 @@ On each node perform the following:
``` ```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the 1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`: `git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "default" => {
'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
``` ```
...@@ -1891,7 +2210,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and ...@@ -1891,7 +2210,7 @@ running [Prometheus](../monitoring/prometheus/index.md) and
The following IP will be used as an example: The following IP will be used as an example:
- `10.6.0.121`: Prometheus - `10.6.0.151`: Prometheus
To configure the Monitoring node: To configure the Monitoring node:
......
...@@ -19,79 +19,107 @@ costly-to-operate environment by using the ...@@ -19,79 +19,107 @@ costly-to-operate environment by using the
[2,000-user reference architecture](2k_users.md). [2,000-user reference architecture](2k_users.md).
> - **Supported users (approximate):** 5,000 > - **Supported users (approximate):** 5,000
> - **High Availability:** Yes > - **High Availability:** Yes ([Praefect](#configure-praefect-postgresql) needs a third-party PostgreSQL solution for HA)
> - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS > - **Test requests per second (RPS) rates:** API: 100 RPS, Web: 10 RPS, Git (Pull): 10 RPS, Git (Push): 2 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|----------------|-------------|----------| |--------------------------------------------|-------------|-------------------------|----------------|-------------|----------|
| External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | External load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | Redis | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
| Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Consul + Sentinel | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | PostgreSQL | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Gitaly | 2 (minimum) | 8 vCPU, 30 GB memory | n1-standard-8 | `m5.2xlarge` | D8s v3 | | Gitaly | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
| Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | `m5.large` | D2s v3 | | Praefect | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| GitLab Rails | 3 | 16 vCPU, 14.4 GB memory | n1-highcpu-16 | `c5.4xlarge` | F16s v2 | | Praefect PostgreSQL | 1+* | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | `c5.large` | F2s v2 | | Sidekiq | 4 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | D2s v3 |
| GitLab Rails | 3 | 16 vCPU, 14.4 GB memory | n1-highcpu-16 | c5.4xlarge | F16s v2 |
| Monitoring node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Object storage | n/a | n/a | n/a | n/a | n/a | | Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | `c5.xlarge` | F4s v2 | | NFS server (optional, not recommended) | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
```mermaid ```plantuml
stateDiagram-v2 @startuml 5k
[*] --> LoadBalancer card "**External Load Balancer**" as elb #6a9be7
LoadBalancer --> ApplicationServer card "**Internal Load Balancer**" as ilb #9370DB
ApplicationServer --> BackgroundJobs together {
ApplicationServer --> Gitaly collections "**GitLab Rails** x3" as gitlab #32CD32
ApplicationServer --> Redis collections "**Sidekiq** x4" as sidekiq #ff8dd1
ApplicationServer --> PgBouncer }
PgBouncer --> Database
ApplicationServer --> ObjectStorage together {
BackgroundJobs --> ObjectStorage card "**Prometheus + Grafana**" as monitor #7FFFD4
collections "**Consul** x3" as consul #e76a9b
ApplicationMonitoring -->ApplicationServer }
ApplicationMonitoring -->Redis
ApplicationMonitoring -->PgBouncer card "Gitaly Cluster" as gitaly_cluster {
ApplicationMonitoring -->Database collections "**Praefect** x3" as praefect #FF8C00
ApplicationMonitoring -->BackgroundJobs collections "**Gitaly** x3" as gitaly #FF8C00
card "**Praefect PostgreSQL***\n//Non fault-tolerant//" as praefect_postgres #FF8C00
state Database {
"PG_Primary_Node" praefect -[#FF8C00]-> gitaly
"PG_Secondary_Node_1..2" praefect -[#FF8C00]> praefect_postgres
} }
state Redis { card "Database" as database {
"R_Primary_Node" collections "**PGBouncer** x3" as pgbouncer #4EA7FF
"R_Replica_Node_1..2" card "**PostgreSQL** (Primary)" as postgres_primary #4EA7FF
"R_Consul/Sentinel_1..3" collections "**PostgreSQL** (Secondary) x2" as postgres_secondary #4EA7FF
}
pgbouncer -[#4EA7FF]-> postgres_primary
state Gitaly { postgres_primary .[#4EA7FF]> postgres_secondary
"Gitaly_1..2" }
}
card "redis" as redis {
state BackgroundJobs { collections "**Redis Persistent** x3" as redis_persistent #FF6347
"Sidekiq_1..4" collections "**Redis Cache** x3" as redis_cache #FF6347
} collections "**Redis Persistent Sentinel** x3" as redis_persistent_sentinel #FF6347
collections "**Redis Cache Sentinel** x3"as redis_cache_sentinel #FF6347
state ApplicationServer {
"GitLab_Rails_1..3" redis_persistent <.[#FF6347]- redis_persistent_sentinel
} redis_cache <.[#FF6347]- redis_cache_sentinel
}
state LoadBalancer {
"LoadBalancer_1" cloud "**Object Storage**" as object_storage #white
}
elb -[#6a9be7]-> gitlab
state ApplicationMonitoring { elb -[#6a9be7]--> monitor
"Prometheus"
"Grafana" gitlab -[#32CD32]> sidekiq
} gitlab -[#32CD32]--> ilb
gitlab -[#32CD32]-> object_storage
state PgBouncer { gitlab -[#32CD32]---> redis
"Internal_Load_Balancer" gitlab -[hidden]-> monitor
"PgBouncer_1..3" gitlab -[hidden]-> consul
}
sidekiq -[#ff8dd1]--> ilb
sidekiq -[#ff8dd1]-> object_storage
sidekiq -[#ff8dd1]---> redis
sidekiq -[hidden]-> monitor
sidekiq -[hidden]-> consul
ilb -[#9370DB]-> gitaly_cluster
ilb -[#9370DB]-> database
consul .[#e76a9b]u-> gitlab
consul .[#e76a9b]u-> sidekiq
consul .[#e76a9b]> monitor
consul .[#e76a9b]-> database
consul .[#e76a9b]-> gitaly_cluster
consul .[#e76a9b,norank]--> redis
monitor .[#7FFFD4]u-> gitlab
monitor .[#7FFFD4]u-> sidekiq
monitor .[#7FFFD4]> consul
monitor .[#7FFFD4]-> database
monitor .[#7FFFD4]-> gitaly_cluster
monitor .[#7FFFD4,norank]--> redis
monitor .[#7FFFD4]> ilb
monitor .[#7FFFD4,norank]u--> elb
@enduml
``` ```
The Google Cloud Platform (GCP) architectures were built and tested using the The Google Cloud Platform (GCP) architectures were built and tested using the
...@@ -106,19 +134,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object- ...@@ -106,19 +134,25 @@ uploads, or artifacts), using an [object storage service](#configure-the-object-
is recommended instead of using NFS. Using an object storage service also is recommended instead of using NFS. Using an object storage service also
doesn't require you to provision and maintain a node. doesn't require you to provision and maintain a node.
It's also worth noting that at this time [Praefect requires its own database server](../gitaly/praefect.md#postgresql) and
that to achieve full High Availability a third party PostgreSQL database solution will be required.
We hope to offer a built in solutions for these restrictions in the future but in the meantime a non HA PostgreSQL server
can be set up via Omnibus GitLab, which the above specs reflect. Refer to the following issues for more information: [`omnibus-gitlab#5919`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919) & [`gitaly#3398`](https://gitlab.com/gitlab-org/gitaly/-/issues/3398)
## Setup components ## Setup components
To set up GitLab and its components to accommodate up to 5,000 users: To set up GitLab and its components to accommodate up to 5,000 users:
1. [Configure the external load balancing node](#configure-the-external-load-balancer) 1. [Configure the external load balancer](#configure-the-external-load-balancer)
to handle the load balancing of the GitLab application services nodes. to handle the load balancing of the GitLab application services nodes.
1. [Configure the internal load balancer](#configure-the-internal-load-balancer).
to handle the load balancing of GitLab application internal connections.
1. [Configure Redis](#configure-redis). 1. [Configure Redis](#configure-redis).
1. [Configure Consul and Sentinel](#configure-consul-and-sentinel). 1. [Configure Consul and Sentinel](#configure-consul-and-sentinel).
1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab. 1. [Configure PostgreSQL](#configure-postgresql), the database for GitLab.
1. [Configure PgBouncer](#configure-pgbouncer). 1. [Configure PgBouncer](#configure-pgbouncer).
1. [Configure the internal load balancing node](#configure-the-internal-load-balancer). 1. [Configure Gitaly Cluster](#configure-gitaly-cluster),
1. [Configure Gitaly](#configure-gitaly), provides access to the Git repositories.
which provides access to the Git repositories.
1. [Configure Sidekiq](#configure-sidekiq). 1. [Configure Sidekiq](#configure-sidekiq).
1. [Configure the main GitLab Rails application](#configure-gitlab-rails) 1. [Configure the main GitLab Rails application](#configure-gitlab-rails)
to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all frontend
...@@ -155,6 +189,11 @@ The following list includes descriptions of each server and its assigned IP: ...@@ -155,6 +189,11 @@ The following list includes descriptions of each server and its assigned IP:
- `10.6.0.20`: Internal Load Balancer - `10.6.0.20`: Internal Load Balancer
- `10.6.0.51`: Gitaly 1 - `10.6.0.51`: Gitaly 1
- `10.6.0.52`: Gitaly 2 - `10.6.0.52`: Gitaly 2
- `10.6.0.93`: Gitaly 3
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
- `10.6.0.141`: Praefect PostgreSQL 1 (non HA)
- `10.6.0.71`: Sidekiq 1 - `10.6.0.71`: Sidekiq 1
- `10.6.0.72`: Sidekiq 2 - `10.6.0.72`: Sidekiq 2
- `10.6.0.73`: Sidekiq 3 - `10.6.0.73`: Sidekiq 3
...@@ -285,6 +324,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ...@@ -285,6 +324,71 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
</a> </a>
</div> </div>
## Configure the internal load balancer
The Internal Load Balancer is used to balance any internal connections the GitLab environment requires
such as connections to [PgBouncer](#configure-pgbouncer) and [Praefect](#configure-praefect) (Gitaly Cluster).
Note that it's a separate node from the External Load Balancer and shouldn't have any access externally.
The following IP will be used as an example:
- `10.6.0.40`: Internal Load Balancer
Here's how you could do it with [HAProxy](https://www.haproxy.org/):
```plaintext
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in
bind *:6432
mode tcp
option tcplog
default_backend pgbouncer
frontend internal-praefect-tcp-in
bind *:2305
mode tcp
option tcplog
option clitcpka
default_backend praefect
backend pgbouncer
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
backend praefect
mode tcp
option tcp-check
option srvtcpka
server praefect1 10.6.0.131:2305 check
server praefect2 10.6.0.132:2305 check
server praefect3 10.6.0.133:2305 check
```
Refer to your preferred Load Balancer's documentation for further guidance.
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure Redis ## Configure Redis
Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica** Using [Redis](https://redis.io/) in scalable environment is possible using a **Primary** x **Replica**
...@@ -924,45 +1028,96 @@ The following IPs will be used as an example: ...@@ -924,45 +1028,96 @@ The following IPs will be used as an example:
</a> </a>
</div> </div>
### Configure the internal load balancer ## Configure Gitaly Cluster
If you're running more than one PgBouncer node as recommended, then at this time you'll need to set [Gitaly Cluster](../gitaly/praefect.md) is a GitLab provided and recommended fault tolerant solution for storing Git repositories.
up a TCP internal load balancer to serve each correctly. In this configuration, every Git repository is stored on every Gitaly node in the cluster, with one being designated the primary, and failover occurs automatically if the primary node goes down.
The following IP will be used as an example: The recommended cluster setup includes the following components:
- `10.6.0.20`: Internal Load Balancer - 3 Gitaly nodes: Replicated storage of Git repositories.
- 3 Praefect nodes: Router and transaction manager for Gitaly Cluster.
- 1 Praefect PostgreSQL node: Database server for Praefect. A third-party solution
is required for Praefect database connections to be made highly available.
- 1 load balancer: A load balancer is required for Praefect. The
[internal load balancer](#configure-the-internal-load-balancer) will be used.
Here's how you could do it with [HAProxy](https://www.haproxy.org/): This section will detail how to configure the recommended standard setup in order.
For more advanced setups refer to the [standalone Gitaly Cluster documentation](../gitaly/praefect.md).
```plaintext ### Configure Praefect PostgreSQL
global
log /dev/log local0
log localhost local1 notice
log stdout format raw local0
defaults Praefect, the routing and transaction manager for Gitaly Cluster, requires its own database server to store data on Gitaly Cluster status.
log global
default-server inter 10s fall 3 rise 2
balance leastconn
frontend internal-pgbouncer-tcp-in If you want to have a highly available setup, Praefect requires a third-party PostgreSQL database.
bind *:6432 A built-in solution is being [worked on](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5919).
mode tcp
option tcplog
default_backend pgbouncer #### Praefect non-HA PostgreSQL standalone using Omnibus GitLab
backend pgbouncer The following IPs will be used as an example:
mode tcp
option tcp-check
server pgbouncer1 10.6.0.21:6432 check - `10.6.0.141`: Praefect PostgreSQL
server pgbouncer2 10.6.0.22:6432 check
server pgbouncer3 10.6.0.23:6432 check
```
Refer to your preferred Load Balancer's documentation for further guidance. First, make sure to [install](https://about.gitlab.com/install/)
the Linux GitLab package in the Praefect PostgreSQL node. Following the steps,
install the necessary dependencies from step 1, and add the
GitLab package repository from step 2. When installing GitLab
in the second step, do not supply the `EXTERNAL_URL` value.
1. SSH in to the Praefect PostgreSQL node.
1. Create a strong password to be used for the Praefect PostgreSQL user. Take note of this password as `<praefect_postgresql_password>`.
1. Generate the password hash for the Praefect PostgreSQL username/password pair. This assumes you will use the default
username of `praefect` (recommended). The command will request the password `<praefect_postgresql_password>`
and confirmation. Use the value that is output by this command in the next
step as the value of `<praefect_postgresql_password_hash>`:
```shell
sudo gitlab-ctl pg-password-md5 praefect
```
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except PostgreSQL and Consul
roles ['postgres_role']
repmgr['enable'] = false
patroni['enable'] = false
# PostgreSQL configuration
postgresql['listen_address'] = '0.0.0.0'
postgresql['max_connections'] = 200
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Replace PRAEFECT_POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = "<praefect_postgresql_password_hash>"
# Replace XXX.XXX.XXX.XXX/YY with Network Address
postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/24)
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
postgres_exporter['listen_address'] = '0.0.0.0:9187'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Follow the [post configuration](#praefect-postgresql-post-configuration).
<div align="right"> <div align="right">
<a type="button" class="btn btn-default" href="#setup-components"> <a type="button" class="btn btn-default" href="#setup-components">
...@@ -970,19 +1125,186 @@ Refer to your preferred Load Balancer's documentation for further guidance. ...@@ -970,19 +1125,186 @@ Refer to your preferred Load Balancer's documentation for further guidance.
</a> </a>
</div> </div>
## Configure Gitaly #### Praefect HA PostgreSQL third-party solution
NOTE: [As noted](#configure-praefect-postgresql), a third-party PostgreSQL solution for
[Gitaly Cluster](../gitaly/praefect.md) support Praefect's database is recommended if aiming for full High Availability.
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified There are many third-party solutions for PostgreSQL HA. The solution selected must have the following to work with Praefect:
some Architecture specs will likely change as a result to support the new
and improved designed. - A static IP for all connections that doesn't change on failover.
- [`LISTEN`](https://www.postgresql.org/docs/12/sql-listen.html) SQL functionality must be supported.
Examples of the above could include [Google's Cloud SQL](https://cloud.google.com/sql/docs/postgres/high-availability#normal) or [Amazon RDS](https://aws.amazon.com/rds/).
Once the database is set up, follow the [post configuration](#praefect-postgresql-post-configuration).
#### Praefect PostgreSQL post-configuration
After the Praefect PostgreSQL server has been set up, you'll then need to configure the user and database for Praefect to use.
We recommend the user be named `praefect` and the database `praefect_production`, and these can be configured as standard in PostgreSQL.
The password for the user is the same as the one you configured earlier as `<praefect_postgresql_password>`.
This is how this would work with a Omnibus GitLab PostgreSQL setup:
1. SSH in to the Praefect PostgreSQL node.
1. Connect to the PostgreSQL server with administrative access.
The `gitlab-psql` user should be used here for this as it's added by default in Omnibus.
The database `template1` is used because it is created by default on all PostgreSQL servers.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data, ```shell
specifically the number of projects and those projects' sizes. It's recommended /opt/gitlab/embedded/bin/psql -U gitlab-psql -d template1 -h POSTGRESQL_SERVER_ADDRESS
that a Gitaly server node stores no more than 5 TB of data. Depending on your ```
repository storage requirements, you may require additional Gitaly server nodes.
1. Create the new user `praefect`, replacing `<praefect_postgresql_password>`:
```shell
CREATE ROLE praefect WITH LOGIN CREATEDB PASSWORD <praefect_postgresql_password>;
```
1. Reconnect to the PostgreSQL server, this time as the `praefect` user:
```shell
/opt/gitlab/embedded/bin/psql -U praefect -d template1 -h POSTGRESQL_SERVER_ADDRESS
```
1. Create a new database `praefect_production`:
```shell
CREATE DATABASE praefect_production WITH ENCODING=UTF8;
```
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
### Configure Praefect
Praefect is the router and transaction manager for Gitaly Cluster and all connections to Gitaly go through
it. This section details how to configure it.
Praefect requires several secret tokens to secure communications across the Cluster:
- `<praefect_external_token>`: Used for repositories hosted on your Gitaly cluster and can only be accessed by Gitaly clients that carry this token.
- `<praefect_internal_token>`: Used for replication traffic inside your Gitaly cluster. This is distinct from `praefect_external_token` because Gitaly clients must not be able to access internal nodes of the Praefect cluster directly; that could lead to data loss.
- `<praefect_postgresql_password>`: The Praefect PostgreSQL password defined in the previous section is also required as part of this setup.
Gitaly Cluster nodes are configured in Praefect via a `virtual storage`. Each storage contains
the details of each Gitaly node that makes up the cluster. Each storage is also given a name
and this name is used in several areas of the config. In this guide, the name of the storage will be
`default`. Also, this guide is geared towards new installs, if upgrading an existing environment
to use Gitaly Cluster, you may need to use a different name.
Refer to the [Praefect documentation](../gitaly/praefect.md#praefect) for more info.
The following IPs will be used as an example:
- `10.6.0.131`: Praefect 1
- `10.6.0.132`: Praefect 2
- `10.6.0.133`: Praefect 3
To configure the Praefect nodes, on each one:
1. SSH in to the Praefect server.
1. [Download and install](https://about.gitlab.com/install/) the Omnibus GitLab
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Edit the `/etc/gitlab/gitlab.rb` file to configure Praefect:
```ruby
# Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
puma['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
# Praefect Configuration
praefect['enable'] = true
praefect['listen_addr'] = '0.0.0.0:2305'
gitlab_rails['rake_cache_clear'] = false
gitlab_rails['auto_migrate'] = false
# Configure the Consul agent
consul['enable'] = true
## Enable service discovery for Prometheus
consul['monitoring_service_discovery'] = true
# START user configuration
# Please set the real values as explained in Required Information section
#
# Praefect External Token
# This is needed by clients outside the cluster (like GitLab Shell) to communicate with the Praefect cluster
praefect['auth_token'] = '<praefect_external_token>'
# Praefect Database Settings
praefect['database_host'] = '10.6.0.141'
praefect['database_port'] = 5432
# `no_proxy` settings must always be a direct connection for caching
praefect['database_host_no_proxy'] = '10.6.0.141'
praefect['database_port_no_proxy'] = 5432
praefect['database_dbname'] = 'praefect_production'
praefect['database_user'] = 'praefect'
praefect['database_password'] = '<praefect_postgresql_password>'
# Praefect Virtual Storage config
# Name of storage hash must match storage name in git_data_dirs on GitLab
# server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
praefect['virtual_storages'] = {
'default' => {
'gitaly-1' => {
'address' => 'tcp://10.6.0.91:8075',
'token' => '<praefect_internal_token>',
'primary' => true
},
'gitaly-2' => {
'address' => 'tcp://10.6.0.92:8075',
'token' => '<praefect_internal_token>'
},
'gitaly-3' => {
'address' => 'tcp://10.6.0.93:8075',
'token' => '<praefect_internal_token>'
},
}
}
# Set the network addresses that the exporters will listen on for monitoring
node_exporter['listen_address'] = '0.0.0.0:9100'
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
#
# END user configuration
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
### Configure Gitaly
The [Gitaly](../gitaly/index.md) server nodes that make up the cluster have
requirements that are dependent on data, specifically the number of projects
and those projects' sizes. It's recommended that a Gitaly Cluster stores
no more than 5 TB of data on each node. Depending on your
repository storage requirements, you may require additional Gitaly Clusters.
Due to Gitaly having notable input and output requirements, we strongly Due to Gitaly having notable input and output requirements, we strongly
recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs
...@@ -993,36 +1315,21 @@ adjusted to greater or lesser values depending on the scale of your ...@@ -993,36 +1315,21 @@ adjusted to greater or lesser values depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider, environment's workload. If you're running the environment on a Cloud provider,
refer to their documentation about how to configure IOPS correctly. refer to their documentation about how to configure IOPS correctly.
Be sure to note the following items: Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-cluster-tls-support).
- The GitLab Rails application shards repositories into For configuring Gitaly you should note the following:
[repository storage paths](../repository_storage_paths.md).
- A Gitaly server can host one or more storage paths.
- A GitLab server can use one or more Gitaly server nodes.
- Gitaly addresses must be specified to be correctly resolvable for all Gitaly
clients.
- Gitaly servers must not be exposed to the public internet, as Gitaly's network
traffic is unencrypted by default. The use of a firewall is highly recommended
to restrict access to the Gitaly server. Another option is to
[use TLS](#gitaly-tls-support).
NOTE:
The token referred to throughout the Gitaly documentation is an arbitrary
password selected by the administrator. This token is unrelated to tokens
created for the GitLab API or other similar web API tokens.
This section describes how to configure two Gitaly servers, with the following - `git_data_dirs` should be configured to reflect the storage path for the specific Gitaly node
IPs and domain names: - `auth_token` should be the same as `praefect_internal_token`
- `10.6.0.51`: Gitaly 1 (`gitaly1.internal`) The following IPs will be used as an example:
- `10.6.0.52`: Gitaly 2 (`gitaly2.internal`)
Assumptions about your servers include having the secret token be `gitalysecret`,
and that your GitLab installation has three repository storages:
- `default` on Gitaly 1 - `10.6.0.91`: Gitaly 1
- `storage1` on Gitaly 1 - `10.6.0.92`: Gitaly 2
- `storage2` on Gitaly 2 - `10.6.0.93`: Gitaly 3
On each node: On each node:
...@@ -1032,21 +1339,9 @@ On each node: ...@@ -1032,21 +1339,9 @@ On each node:
1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure 1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure
storage paths, enable the network listener, and to configure the token: storage paths, enable the network listener, and to configure the token:
<!--
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
-->
```ruby ```ruby
# /etc/gitlab/gitlab.rb # /etc/gitlab/gitlab.rb
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
# The following two values must be the same as their respective values
# of the GitLab Rails application setup
gitaly['auth_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
# Avoid running unnecessary services on the Gitaly server # Avoid running unnecessary services on the Gitaly server
postgresql['enable'] = false postgresql['enable'] = false
redis['enable'] = false redis['enable'] = false
...@@ -1056,7 +1351,6 @@ On each node: ...@@ -1056,7 +1351,6 @@ On each node:
sidekiq['enable'] = false sidekiq['enable'] = false
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['enable'] = false grafana['enable'] = false
gitlab_exporter['enable'] = false
# If you run a separate monitoring node you can disable these services # If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false alertmanager['enable'] = false
...@@ -1077,101 +1371,86 @@ On each node: ...@@ -1077,101 +1371,86 @@ On each node:
# Comment out following line if you only want to support TLS connections # Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075" gitaly['listen_addr'] = "0.0.0.0:8075"
## Enable service discovery for Prometheus # Gitaly Auth Token
consul['enable'] = true # Should be the same as praefect_internal_token
consul['monitoring_service_discovery'] = true gitaly['auth_token'] = '<praefect_internal_token>'
# Set the network addresses that the exporters will listen on for monitoring
gitaly['prometheus_listen_addr'] = "0.0.0.0:9236"
node_exporter['listen_address'] = '0.0.0.0:9100'
gitlab_rails['prometheus_address'] = '10.6.0.81:9090'
## The IPs of the Consul server nodes
## You can also use FQDNs and intermix them with IPs
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13),
}
``` ```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- On `gitaly1.internal`: - On Gitaly node 1:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { "gitaly-1" => {
'path' => '/var/opt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
'storage1' => {
'path' => '/mnt/gitlab/git-data'
},
}) })
``` ```
- On `gitaly2.internal`: - On Gitaly node 2:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'storage2' => { "gitaly-2" => {
'path' => '/mnt/gitlab/git-data' "path" => "/var/opt/gitlab/git-data"
}, }
}) })
``` ```
<!-- - On Gitaly node 3:
updates to following example must also be made at
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab ```ruby
--> git_data_dirs({
"gitaly-3" => {
"path" => "/var/opt/gitlab/git-data"
}
})
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and
then replace the file of the same name on this server. If that file isn't on
this server, add the file from your Consul server to this server.
1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Confirm that Gitaly can perform callbacks to the internal API:
```shell ### Gitaly Cluster TLS support
sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml
```
1. Verify the GitLab services are running: Praefect supports TLS encryption. To communicate with a Praefect instance that listens
for secure connections, you must:
```shell - Use a `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry
sudo gitlab-ctl status in the GitLab configuration.
``` - Bring your own certificates because this isn't provided automatically. The certificate
corresponding to each Praefect server must be installed on that Praefect server.
The output should be similar to the following: Additionally the certificate, or its certificate authority, must be installed on all Gitaly servers
and on all Praefect clients that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) (and repeated below).
```plaintext Note the following:
run: consul: (pid 30339) 77006s; run: log: (pid 29878) 77020s
run: gitaly: (pid 30351) 77005s; run: log: (pid 29660) 77040s
run: logrotate: (pid 7760) 3213s; run: log: (pid 29782) 77032s
run: node-exporter: (pid 30378) 77004s; run: log: (pid 29812) 77026s
```
### Gitaly TLS support - The certificate must specify the address you use to access the Praefect server. If
addressing the Praefect server by:
Gitaly supports TLS encryption. To be able to communicate - Hostname, you can either use the Common Name field for this, or add it as a Subject
with a Gitaly instance that listens for secure connections you will need to use `tls://` URL Alternative Name.
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration. - IP address, you must add it as a Subject Alternative Name to the certificate.
You will need to bring your own certificates as this isn't provided automatically. - You can configure Praefect servers with both an unencrypted listening address
The certificate, or its certificate authority, must be installed on all Gitaly `listen_addr` and an encrypted listening address `tls_listen_addr` at the same time.
nodes (including the Gitaly node using the certificate) and on all client nodes This allows you to do a gradual transition from unencrypted to encrypted traffic, if
that communicate with it following the procedure described in necessary.
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: - The Internal Load Balancer will also access to the certificates and need to be configured
The self-signed certificate must specify the address you use to access the to allow for TLS passthrough.
Gitaly server. If you are addressing the Gitaly server by a hostname, you can Refer to the load balancers documentation on how to configure this.
either use the Common Name field for this, or add it as a Subject Alternative
Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
It's possible to configure Gitaly servers with both an unencrypted listening To configure Praefect with TLS:
address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
at the same time. This allows you to do a gradual transition from unencrypted to
encrypted traffic, if necessary.
To configure Gitaly with TLS: 1. Create certificates for Praefect servers.
1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: 1. On the Praefect servers, create the `/etc/gitlab/ssl` directory and copy your key
and certificate there:
```shell ```shell
sudo mkdir -p /etc/gitlab/ssl sudo mkdir -p /etc/gitlab/ssl
...@@ -1180,27 +1459,35 @@ To configure Gitaly with TLS: ...@@ -1180,27 +1459,35 @@ To configure Gitaly with TLS:
sudo chmod 644 key.pem cert.pem sudo chmod 644 key.pem cert.pem
``` ```
1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when 1. Edit `/etc/gitlab/gitlab.rb` and add:
calling into itself:
```shell ```ruby
sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/ praefect['tls_listen_addr'] = "0.0.0.0:3305"
praefect['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
praefect['key_path'] = "/etc/gitlab/ssl/key.pem"
``` ```
1. Edit `/etc/gitlab/gitlab.rb` and add: 1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<!-- 1. On the Praefect clients (including each Gitaly server), copy the certificates,
updates to following example must also be made at or their certificate authority, into `/etc/gitlab/trusted-certs`:
https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
--> ```shell
sudo cp cert.pem /etc/gitlab/trusted-certs/
```
1. On the Praefect clients (except Gitaly servers), edit `git_data_dirs` in
`/etc/gitlab/gitlab.rb` as follows:
```ruby ```ruby
gitaly['tls_listen_addr'] = "0.0.0.0:9999" git_data_dirs({
gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem" "default" => {
gitaly['key_path'] = "/etc/gitlab/ssl/key.pem" "gitaly_address" => 'tls://LOAD_BALANCER_SERVER_ADDRESS:2305',
"gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
}
})
``` ```
1. Delete `gitaly['listen_addr']` to allow only encrypted connections.
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
<div align="right"> <div align="right">
...@@ -1269,17 +1556,20 @@ To configure the Sidekiq nodes, one each one: ...@@ -1269,17 +1556,20 @@ To configure the Sidekiq nodes, one each one:
### Gitaly ### ### Gitaly ###
####################################### #######################################
# git_data_dirs get configured for the Praefect virtual storage
# Address is Internal Load Balancer for Praefect
# Token is praefect_external_token
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
gitlab_rails['gitaly_token'] = 'YOUR_TOKEN'
####################################### #######################################
### Postgres ### ### Postgres ###
####################################### #######################################
gitlab_rails['db_host'] = '10.6.0.20' # internal load balancer IP gitlab_rails['db_host'] = '10.6.0.40' # internal load balancer IP
gitlab_rails['db_port'] = 6432 gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = '<postgresql_user_password>' gitlab_rails['db_password'] = '<postgresql_user_password>'
gitlab_rails['db_adapter'] = 'postgresql' gitlab_rails['db_adapter'] = 'postgresql'
...@@ -1397,17 +1687,14 @@ On each node perform the following: ...@@ -1397,17 +1687,14 @@ On each node perform the following:
```ruby ```ruby
external_url 'https://gitlab.example.com' external_url 'https://gitlab.example.com'
# Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests # git_data_dirs get configured for the Praefect virtual storage
# to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API. # Address is Interal Load Balancer for Praefect
# The following two values must be the same as their respective values # Token is praefect_external_token
# of the Gitaly setup
gitlab_rails['gitaly_token'] = 'gitalysecret'
gitlab_shell['secret_token'] = 'shellsecret'
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "default" => {
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, "gitaly_address" => "tcp://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
## Disable components that will not be on the GitLab application server ## Disable components that will not be on the GitLab application server
...@@ -1495,14 +1782,15 @@ On each node perform the following: ...@@ -1495,14 +1782,15 @@ On each node perform the following:
#registry['gid'] = 9002 #registry['gid'] = 9002
``` ```
1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the 1. If you're using [Gitaly with TLS support](#gitaly-cluster-tls-support), make sure the
`git_data_dirs` entry is configured with `tls` instead of `tcp`: `git_data_dirs` entry is configured with `tls` instead of `tcp`:
```ruby ```ruby
git_data_dirs({ git_data_dirs({
'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "default" => {
'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, "gitaly_address" => "tls://10.6.0.40:2305", # internal load balancer IP
'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' }, "gitaly_token" => '<praefect_external_token>'
}
}) })
``` ```
......
...@@ -137,10 +137,10 @@ To execute a pipeline manually: ...@@ -137,10 +137,10 @@ To execute a pipeline manually:
1. Navigate to your project's **CI/CD > Pipelines**. 1. Navigate to your project's **CI/CD > Pipelines**.
1. Select the **Run Pipeline** button. 1. Select the **Run Pipeline** button.
1. On the **Run Pipeline** page: 1. On the **Run Pipeline** page:
1. Select the branch to run the pipeline for in the **Create for** field. 1. Select the branch or tag to run the pipeline for in the **Run for branch name or tag** field.
1. Enter any [environment variables](../variables/README.md) required for the pipeline run. 1. Enter any [environment variables](../variables/README.md) required for the pipeline run.
You can set specific variables to have their [values prefilled in the form](#prefill-variables-in-manual-pipelines). You can set specific variables to have their [values prefilled in the form](#prefill-variables-in-manual-pipelines).
1. Click the **Create pipeline** button. 1. Click the **Run pipeline** button.
The pipeline now executes the jobs as configured. The pipeline now executes the jobs as configured.
......
...@@ -46,7 +46,7 @@ The keywords available for jobs are: ...@@ -46,7 +46,7 @@ The keywords available for jobs are:
| [`only`](#onlyexcept-basic) | Limit when jobs are created. | | [`only`](#onlyexcept-basic) | Limit when jobs are created. |
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. | | [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. | | [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
| [`release`](#release) | Instructs the runner to generate a [Release](../../user/project/releases/index.md) object. | | [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
| [`resource_group`](#resource_group) | Limit job concurrency. | | [`resource_group`](#resource_group) | Limit job concurrency. |
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. | | [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. | | [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
...@@ -79,7 +79,7 @@ You can't use these keywords as job names: ...@@ -79,7 +79,7 @@ You can't use these keywords as job names:
You can set global defaults for some keywords. Jobs that do not define one or more You can set global defaults for some keywords. Jobs that do not define one or more
of the listed keywords use the value defined in the `default:` section. of the listed keywords use the value defined in the `default:` section.
The following job keywords can be defined inside a `default:` section: These job keywords can be defined inside a `default:` section:
- [`after_script`](#after_script) - [`after_script`](#after_script)
- [`artifacts`](#artifacts) - [`artifacts`](#artifacts)
...@@ -92,7 +92,7 @@ The following job keywords can be defined inside a `default:` section: ...@@ -92,7 +92,7 @@ The following job keywords can be defined inside a `default:` section:
- [`tags`](#tags) - [`tags`](#tags)
- [`timeout`](#timeout) - [`timeout`](#timeout)
This example sets the `ruby:2.5` image as the default for all jobs in the pipeline. The following example sets the `ruby:2.5` image as the default for all jobs in the pipeline.
The `rspec 2.6` job does not use the default, because it overrides the default with The `rspec 2.6` job does not use the default, because it overrides the default with
a job-specific `image:` section: a job-specific `image:` section:
...@@ -159,9 +159,9 @@ the [`needs`](#needs) keyword. ...@@ -159,9 +159,9 @@ the [`needs`](#needs) keyword.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5 > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5
The top-level `workflow:` keyword determines whether or not a pipeline is created. Use `workflow:` to determine whether or not a pipeline is created.
It accepts a single `rules:` keyword that is similar to [`rules:` defined in jobs](#rules). Define this keyword at the top level, with a single `rules:` keyword that
Use it to define what can trigger a new pipeline. is similar to [`rules:` defined in jobs](#rules).
You can use the [`workflow:rules` templates](#workflowrules-templates) to import You can use the [`workflow:rules` templates](#workflowrules-templates) to import
a preconfigured `workflow: rules` entry. a preconfigured `workflow: rules` entry.
...@@ -186,7 +186,7 @@ Some example `if` clauses for `workflow: rules`: ...@@ -186,7 +186,7 @@ Some example `if` clauses for `workflow: rules`:
See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples. See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
For example, in the following configuration, pipelines run for all `push` events (changes to In the following example, pipelines run for all `push` events (changes to
branches and new tags). Pipelines for push events with `-draft` in the commit message branches and new tags). Pipelines for push events with `-draft` in the commit message
don't run, because they are set to `when: never`. Pipelines for schedules or merge requests don't run, because they are set to `when: never`. Pipelines for schedules or merge requests
don't run either, because no rules evaluate to true for them: don't run either, because no rules evaluate to true for them:
...@@ -226,7 +226,7 @@ If your rules match both branch pipelines and merge request pipelines, ...@@ -226,7 +226,7 @@ If your rules match both branch pipelines and merge request pipelines,
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217732) in GitLab 13.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217732) in GitLab 13.0.
We provide templates that set up `workflow: rules` GitLab provides templates that set up `workflow: rules`
for common scenarios. These templates help prevent duplicate pipelines. for common scenarios. These templates help prevent duplicate pipelines.
The [`Branch-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml) The [`Branch-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml)
...@@ -234,12 +234,12 @@ makes your pipelines run for branches and tags. ...@@ -234,12 +234,12 @@ makes your pipelines run for branches and tags.
Branch pipeline status is displayed in merge requests that use the branch Branch pipeline status is displayed in merge requests that use the branch
as a source. However, this pipeline type does not support any features offered by as a source. However, this pipeline type does not support any features offered by
[Merge Request Pipelines](../merge_request_pipelines/), like [merge request pipelines](../merge_request_pipelines/), like
[Pipelines for Merge Results](../merge_request_pipelines/#pipelines-for-merged-results) [pipelines for merge results](../merge_request_pipelines/#pipelines-for-merged-results)
or [Merge Trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/). or [merge trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/).
Use this template if you are intentionally avoiding those features. This template intentionally avoids those features.
It is [included](#include) as follows: To [include](#include) it:
```yaml ```yaml
include: include:
...@@ -249,10 +249,9 @@ include: ...@@ -249,10 +249,9 @@ include:
The [`MergeRequest-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml) The [`MergeRequest-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml)
makes your pipelines run for the default branch, tags, and makes your pipelines run for the default branch, tags, and
all types of merge request pipelines. Use this template if you use any of the all types of merge request pipelines. Use this template if you use any of the
the [Pipelines for Merge Requests features](../merge_request_pipelines/), as mentioned the [pipelines for merge requests features](../merge_request_pipelines/).
above.
It is [included](#include) as follows: To [include](#include) it:
```yaml ```yaml
include: include:
...@@ -317,7 +316,7 @@ does not block triggered pipelines. ...@@ -317,7 +316,7 @@ does not block triggered pipelines.
> - Available for Starter, Premium, and Ultimate in GitLab 10.6 and later. > - Available for Starter, Premium, and Ultimate in GitLab 10.6 and later.
> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4. > - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4.
Use the `include` keyword to include external YAML files in your CI/CD configuration. Use `include` to include external YAML files in your CI/CD configuration.
You can break down one long `gitlab-ci.yml` file into multiple files to increase readability, You can break down one long `gitlab-ci.yml` file into multiple files to increase readability,
or reduce duplication of the same configuration in multiple places. or reduce duplication of the same configuration in multiple places.
...@@ -339,11 +338,11 @@ YAML files, use [`!reference` tags](#reference-tags) or the [`extends` keyword]( ...@@ -339,11 +338,11 @@ YAML files, use [`!reference` tags](#reference-tags) or the [`extends` keyword](
| [`remote`](#includeremote) | Include a file from a remote URL. Must be publicly accessible. | | [`remote`](#includeremote) | Include a file from a remote URL. Must be publicly accessible. |
| [`template`](#includetemplate) | Include templates that are provided by GitLab. | | [`template`](#includetemplate) | Include templates that are provided by GitLab. |
The `.gitlab-ci.yml` file configuration included by all methods is evaluated when the pipeline is created. When the pipeline starts, the `.gitlab-ci.yml` file configuration included by all methods is evaluated.
The configuration is a snapshot in time and persisted in the database. Any changes to The configuration is a snapshot in time and persists in the database. GitLab does not reflect any changes to
the referenced `.gitlab-ci.yml` file configuration is not reflected in GitLab until the next pipeline is created. the referenced `.gitlab-ci.yml` file configuration until the next pipeline starts.
The files defined by `include` are: The `include` files are:
- Deep merged with those in the `.gitlab-ci.yml` file. - Deep merged with those in the `.gitlab-ci.yml` file.
- Always evaluated first and merged with the content of the `.gitlab-ci.yml` file, - Always evaluated first and merged with the content of the `.gitlab-ci.yml` file,
...@@ -367,13 +366,13 @@ include: ...@@ -367,13 +366,13 @@ include:
file: '.compliance-gitlab-ci.yml' file: '.compliance-gitlab-ci.yml'
``` ```
For an example of how you can include these predefined variables, and their impact on CI jobs, For an example of how you can include these predefined variables, and the variables' impact on CI/CD jobs,
see the following [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos). see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
#### `include:local` #### `include:local`
`include:local` includes a file that is in the same repository as the `.gitlab-ci.yml` file. Use `include:local` to include a file that is in the same repository as the `.gitlab-ci.yml` file.
It's referenced with full paths relative to the root directory (`/`). Use a full path relative to the root directory (`/`).
If you use `include:local`, make sure that both the `.gitlab-ci.yml` file and the local file If you use `include:local`, make sure that both the `.gitlab-ci.yml` file and the local file
are on the same branch. are on the same branch.
...@@ -390,7 +389,7 @@ include: ...@@ -390,7 +389,7 @@ include:
- local: '/templates/.gitlab-ci-template.yml' - local: '/templates/.gitlab-ci-template.yml'
``` ```
This can be defined as a short local include: You can also use shorter syntax to define the path:
```yaml ```yaml
include: '.gitlab-ci-production.yml' include: '.gitlab-ci-production.yml'
...@@ -404,8 +403,9 @@ Use local includes instead of symbolic links. ...@@ -404,8 +403,9 @@ Use local includes instead of symbolic links.
To include files from another private project on the same GitLab instance, To include files from another private project on the same GitLab instance,
use `include:file`. You can use `include:file` in combination with `include:project` only. use `include:file`. You can use `include:file` in combination with `include:project` only.
Use a full path, relative to the root directory (`/`).
The included file is referenced with a full path, relative to the root directory (`/`). For example: For example:
```yaml ```yaml
include: include:
...@@ -413,7 +413,7 @@ include: ...@@ -413,7 +413,7 @@ include:
file: '/templates/.gitlab-ci-template.yml' file: '/templates/.gitlab-ci-template.yml'
``` ```
You can also specify a `ref`. If not specified, it defaults to the `HEAD` of the project: You can also specify a `ref`. If you do not specify a value, the ref defaults to the `HEAD` of the project:
```yaml ```yaml
include: include:
...@@ -502,15 +502,15 @@ to resolve all files is 30 seconds. ...@@ -502,15 +502,15 @@ to resolve all files is 30 seconds.
#### Additional `includes` examples #### Additional `includes` examples
There is a list of [additional `includes` examples](includes.md) available. View [additional `includes` examples](includes.md).
## Keyword details ## Keyword details
The following are detailed explanations for keywords used to configure CI/CD pipelines. The following topics explain how to use keywords to configure CI/CD pipelines.
### `image` ### `image`
Used to specify [a Docker image](../docker/using_docker_images.md#what-is-an-image) to use for the job. Use `image` to specify [a Docker image](../docker/using_docker_images.md#what-is-an-image) to use for the job.
For: For:
...@@ -531,13 +531,13 @@ For more information, see [Available settings for `image`](../docker/using_docke ...@@ -531,13 +531,13 @@ For more information, see [Available settings for `image`](../docker/using_docke
#### `services` #### `services`
Used to specify a [service Docker image](../docker/using_docker_images.md#what-is-a-service), linked to a base image specified in [`image`](#image). Use `services` to specify a [service Docker image](../docker/using_docker_images.md#what-is-a-service), linked to a base image specified in [`image`](#image).
For: For:
- Usage examples, see [Define `image` and `services` from `.gitlab-ci.yml`](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml). - Usage examples, see [Define `image` and `services` from `.gitlab-ci.yml`](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml).
- Detailed usage information, refer to [Docker integration](../docker/index.md) documentation. - Detailed usage information, refer to [Docker integration](../docker/index.md) documentation.
- For example services, see [GitLab CI/CD Services](../services/index.md). - Example services, see [GitLab CI/CD Services](../services/index.md).
##### `services:name` ##### `services:name`
...@@ -565,8 +565,11 @@ For more information, see [Available settings for `services`](../docker/using_do ...@@ -565,8 +565,11 @@ For more information, see [Available settings for `services`](../docker/using_do
### `script` ### `script`
`script` is the only required keyword that a job needs. It's a shell script Use `script` to specify a shell script for the runner to execute.
that is executed by the runner. For example:
All jobs except [trigger jobs](#trigger) require a `script` keyword.
For example:
```yaml ```yaml
job: job:
...@@ -575,7 +578,7 @@ job: ...@@ -575,7 +578,7 @@ job:
You can use [YAML anchors with `script`](#yaml-anchors-for-scripts). You can use [YAML anchors with `script`](#yaml-anchors-for-scripts).
This keyword can also contain several commands in an array: The `script` keyword can also contain several commands in an array:
```yaml ```yaml
job: job:
...@@ -609,7 +612,7 @@ job: ...@@ -609,7 +612,7 @@ job:
You can verify the syntax is valid with the [CI Lint](../lint.md) tool. You can verify the syntax is valid with the [CI Lint](../lint.md) tool.
Be careful when using these special characters as well: Be careful when using these characters as well:
- `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``. - `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``.
...@@ -629,10 +632,10 @@ job: ...@@ -629,10 +632,10 @@ job:
Use `before_script` to define an array of commands that should run before each job, Use `before_script` to define an array of commands that should run before each job,
but after [artifacts](#artifacts) are restored. but after [artifacts](#artifacts) are restored.
Scripts specified in `before_script` are concatenated with any scripts specified Scripts you specify in `before_script` are concatenated with any scripts you specify
in the main [`script`](#script), and executed together in a single shell. in the main [`script`](#script). The combine scripts execute together in a single shell.
It's possible to overwrite a globally defined `before_script` if you define it in a job: You can overwrite a globally-defined `before_script` if you define it in a job:
```yaml ```yaml
default: default:
...@@ -657,11 +660,11 @@ You can use [YAML anchors with `before_script`](#yaml-anchors-for-scripts). ...@@ -657,11 +660,11 @@ You can use [YAML anchors with `before_script`](#yaml-anchors-for-scripts).
Use `after_script` to define an array of commands that run after each job, Use `after_script` to define an array of commands that run after each job,
including failed jobs. including failed jobs.
If a job times out or is cancelled, the `after_script` commands are not executed. If a job times out or is cancelled, the `after_script` commands do not execute.
Support for executing `after_script` commands for timed-out or cancelled jobs An [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/15603) exists to support
[is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/15603). executing `after_script` commands for timed-out or cancelled jobs.
Scripts specified in `after_script` are executed in a new shell, separate from any Scripts you specify in `after_script` execute in a new shell, separate from any
`before_script` or `script` scripts. As a result, they: `before_script` or `script` scripts. As a result, they:
- Have a current working directory set back to the default. - Have a current working directory set back to the default.
...@@ -694,7 +697,7 @@ You can use [YAML anchors with `after_script`](#yaml-anchors-for-scripts). ...@@ -694,7 +697,7 @@ You can use [YAML anchors with `after_script`](#yaml-anchors-for-scripts).
#### Script syntax #### Script syntax
You can use special syntax in [`script`](README.md#script) sections to: You can use syntax in [`script`](README.md#script) sections to:
- [Split long commands](script.md#split-long-commands) into multiline commands. - [Split long commands](script.md#split-long-commands) into multiline commands.
- [Use color codes](script.md#add-color-codes-to-script-output) to make job logs easier to review. - [Use color codes](script.md#add-color-codes-to-script-output) to make job logs easier to review.
...@@ -703,9 +706,19 @@ You can use special syntax in [`script`](README.md#script) sections to: ...@@ -703,9 +706,19 @@ You can use special syntax in [`script`](README.md#script) sections to:
### `stage` ### `stage`
`stage` is defined per-job and relies on [`stages`](#stages), which is defined Use `stage` to define which stage a job runs in. Jobs in the same
globally. Use `stage` to define which stage a job runs in, and jobs of the same `stage` can execute in parallel (subject to [certain conditions](#use-your-own-runners)).
`stage` are executed in parallel (subject to [certain conditions](#use-your-own-runners)). For example:
Jobs without a `stage` entry use the `test` stage by default. If [`stages`](#stages)
is not defined in the pipeline, you can use the 5 default stages, which execute in
this order:
- [`.pre`](#pre-and-post)
- `build`
- `test`
- `deploy`
- [`.post`](#pre-and-post)
For example:
```yaml ```yaml
stages: stages:
...@@ -751,45 +764,39 @@ is greater than `1`. ...@@ -751,45 +764,39 @@ is greater than `1`.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31441) in GitLab 12.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31441) in GitLab 12.4.
The following stages are available to every pipeline: Use `pre` and `post` for jobs that need to run first or last in a pipeline.
- `.pre`, which is guaranteed to always be the first stage in a pipeline. - `.pre` is guaranteed to always be the first stage in a pipeline.
- `.post`, which is guaranteed to always be the last stage in a pipeline. - `.post` is guaranteed to always be the last stage in a pipeline.
User-defined stages are executed after `.pre` and before `.post`. User-defined stages are executed after `.pre` and before `.post`.
A pipeline is not created if all jobs are in `.pre` or `.post` stages. You must have a job in at least one stage other than `.pre` or `.post`.
The order of `.pre` and `.post` can't be changed, even if defined out of order in the `.gitlab-ci.yml` file.
For example, the following are equivalent configuration:
- Configured in order:
```yaml You can't change the order of `.pre` and `.post`, even if you define them out of order in the `.gitlab-ci.yml` file.
stages: For example, the following configurations are equivalent:
- .pre
- a
- b
- .post
```
- Configured out of order:
```yaml ```yaml
stages: stages:
- a - .pre
- .pre - a
- b - b
- .post - .post
``` ```
- Not explicitly configured: ```yaml
stages:
- a
- .pre
- b
- .post
```
```yaml ```yaml
stages: stages:
- a - a
- b - b
``` ```
### `extends` ### `extends`
...@@ -799,7 +806,7 @@ Use `extends` to reuse configuration sections. It's an alternative to [YAML anch ...@@ -799,7 +806,7 @@ Use `extends` to reuse configuration sections. It's an alternative to [YAML anch
and is a little more flexible and readable. You can use `extends` to reuse configuration and is a little more flexible and readable. You can use `extends` to reuse configuration
from [included configuration files](#use-extends-and-include-together). from [included configuration files](#use-extends-and-include-together).
In this example, the `rspec` job uses the configuration from the `.tests` template job. In the following example, the `rspec` job uses the configuration from the `.tests` template job.
GitLab: GitLab:
- Performs a reverse deep merge based on the keys. - Performs a reverse deep merge based on the keys.
...@@ -870,8 +877,8 @@ In GitLab 12.0 and later, it's also possible to use multiple parents for ...@@ -870,8 +877,8 @@ In GitLab 12.0 and later, it's also possible to use multiple parents for
#### Merge details #### Merge details
`extends` is able to merge hashes but not arrays. You can use `extends` to merge hashes but not arrays.
The algorithm used for merge is "closest scope wins", so The algorithm used for merge is "closest scope wins," so
keys from the last member always override anything defined on other keys from the last member always override anything defined on other
levels. For example: levels. For example:
...@@ -923,7 +930,7 @@ rspec: ...@@ -923,7 +930,7 @@ rspec:
- rake rspec - rake rspec
``` ```
Note that in the example above: In this example:
- The `variables` sections merge, but `URL: "http://docker-url.internal"` overwrites `URL: "http://my-url.internal"`. - The `variables` sections merge, but `URL: "http://docker-url.internal"` overwrites `URL: "http://my-url.internal"`.
- `tags: ['docker']` overwrites `tags: ['production']`. - `tags: ['docker']` overwrites `tags: ['production']`.
...@@ -935,8 +942,8 @@ Note that in the example above: ...@@ -935,8 +942,8 @@ Note that in the example above:
To reuse configuration from different configuration files, To reuse configuration from different configuration files,
combine `extends` and [`include`](#include). combine `extends` and [`include`](#include).
In this example, a `script` is defined in the `included.yml` file. In the following example, a `script` is defined in the `included.yml` file.
Then, in the `.gitlab-ci.yml` file, you use `extends` to refer Then, in the `.gitlab-ci.yml` file, `extends` refers
to the contents of the `script`: to the contents of the `script`:
- `included.yml`: - `included.yml`:
...@@ -961,11 +968,11 @@ to the contents of the `script`: ...@@ -961,11 +968,11 @@ to the contents of the `script`:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27863) in GitLab 12.3. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27863) in GitLab 12.3.
Use the `rules` keyword to include or exclude jobs in pipelines. Use `rules` to include or exclude jobs in pipelines.
Rules are evaluated *in order* until the first match. When matched, the job Rules are evaluated *in order* until the first match. When a match is found, the job
is either included or excluded from the pipeline, depending on the configuration. is either included or excluded from the pipeline, depending on the configuration.
If included, the job also has [certain attributes](#rules-attributes) The job can also have [certain attributes](#rules-attributes)
added to it. added to it.
`rules` replaces [`only/except`](#onlyexcept-basic) and they can't be used together `rules` replaces [`only/except`](#onlyexcept-basic) and they can't be used together
...@@ -1024,7 +1031,7 @@ The job is not added to the pipeline: ...@@ -1024,7 +1031,7 @@ The job is not added to the pipeline:
`when: always`. `when: always`.
- If a rule matches, and has `when: never` as the attribute. - If a rule matches, and has `when: never` as the attribute.
This example uses `if` to strictly limit when jobs run: The following example uses `if` to strictly limit when jobs run:
```yaml ```yaml
job: job:
...@@ -1101,7 +1108,7 @@ other pipelines, including **both** push (branch) and merge request pipelines. W ...@@ -1101,7 +1108,7 @@ other pipelines, including **both** push (branch) and merge request pipelines. W
this configuration, every push to an open merge request's source branch this configuration, every push to an open merge request's source branch
causes duplicated pipelines. causes duplicated pipelines.
There are multiple ways to avoid duplicate pipelines: To avoid duplicate pipelines, you can:
- Use [`workflow`](#workflow) to specify which types of pipelines - Use [`workflow`](#workflow) to specify which types of pipelines
can run. can run.
...@@ -1164,12 +1171,11 @@ runs in all cases except merge requests. ...@@ -1164,12 +1171,11 @@ runs in all cases except merge requests.
#### `rules:if` #### `rules:if`
`rules:if` clauses determine whether or not jobs are added to a pipeline by evaluating Use `rules:if` clauses to specify when to add a job to a pipeline:
an `if` statement. If the `if` statement is true, the job is either included
or excluded from a pipeline. In plain English, `if` rules can be interpreted as one of:
- "If this rule evaluates to true, add the job" (default). - If an `if` statement is true, add the job to the pipeline.
- "If this rule evaluates to true, do not add the job" (by adding `when: never`). - If an `if` statement is true, but it's combined with `when: never`, do not add the job to the pipeline.
- If no `if` statements are true, do not add the job to the pipeline.
`rules:if` differs slightly from `only:variables` by accepting only a single `rules:if` differs slightly from `only:variables` by accepting only a single
expression string per rule, rather than an array of them. Any set of expressions to be expression string per rule, rather than an array of them. Any set of expressions to be
...@@ -1227,7 +1233,9 @@ check the value of the `$CI_PIPELINE_SOURCE` variable: ...@@ -1227,7 +1233,9 @@ check the value of the `$CI_PIPELINE_SOURCE` variable:
| `web` | For pipelines created by using **Run pipeline** button in the GitLab UI, from the project's **CI/CD > Pipelines** section. | | `web` | For pipelines created by using **Run pipeline** button in the GitLab UI, from the project's **CI/CD > Pipelines** section. |
| `webide` | For pipelines created by using the [WebIDE](../../user/project/web_ide/index.md). | | `webide` | For pipelines created by using the [WebIDE](../../user/project/web_ide/index.md). |
For example: The following example runs the job as a manual job in scheduled pipelines or in push
pipelines (to branches or tags), with `when: on_success` (default). It does not
add the job to any other pipeline type.
```yaml ```yaml
job: job:
...@@ -1239,11 +1247,8 @@ job: ...@@ -1239,11 +1247,8 @@ job:
- if: '$CI_PIPELINE_SOURCE == "push"' - if: '$CI_PIPELINE_SOURCE == "push"'
``` ```
This example runs the job as a manual job in scheduled pipelines or in push The following example runs the job as a `when: on_success` job in [merge request pipelines](../merge_request_pipelines/index.md)
pipelines (to branches or tags), with `when: on_success` (default). It does not and scheduled pipelines. It does not run in any other pipeline type.
add the job to any other pipeline type.
Another example:
```yaml ```yaml
job: job:
...@@ -1253,9 +1258,6 @@ job: ...@@ -1253,9 +1258,6 @@ job:
- if: '$CI_PIPELINE_SOURCE == "schedule"' - if: '$CI_PIPELINE_SOURCE == "schedule"'
``` ```
This example runs the job as a `when: on_success` job in [merge request pipelines](../merge_request_pipelines/index.md)
and scheduled pipelines. It does not run in any other pipeline type.
Other commonly used variables for `if` clauses: Other commonly used variables for `if` clauses:
- `if: $CI_COMMIT_TAG`: If changes are pushed for a tag. - `if: $CI_COMMIT_TAG`: If changes are pushed for a tag.
...@@ -1272,11 +1274,11 @@ Other commonly used variables for `if` clauses: ...@@ -1272,11 +1274,11 @@ Other commonly used variables for `if` clauses:
#### `rules:changes` #### `rules:changes`
`rules:changes` determines whether or not to add jobs to a pipeline by checking for Use `rules:changes` to specify when to add a job to a pipeline by checking for
changes to specific files. changes to specific files.
`rules: changes` works exactly the same way as [`only: changes` and `except: changes`](#onlychangesexceptchanges), `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychangesexceptchanges).
accepting an array of paths. It's recommended to only use `rules: changes` with branch It accepts an array of paths. You should use `rules: changes` only with branch
pipelines or merge request pipelines. For example, it's common to use `rules: changes` pipelines or merge request pipelines. For example, it's common to use `rules: changes`
with merge request pipelines: with merge request pipelines:
...@@ -1299,7 +1301,7 @@ In this example: ...@@ -1299,7 +1301,7 @@ In this example:
- If `Dockerfile` has not changed, do not add job to any pipeline (same as `when: never`). - If `Dockerfile` has not changed, do not add job to any pipeline (same as `when: never`).
To use `rules: changes` with branch pipelines instead of merge request pipelines, To use `rules: changes` with branch pipelines instead of merge request pipelines,
change the `if:` clause in the example above to: change the `if:` clause in the previous example to:
```yaml ```yaml
rules: rules:
...@@ -1321,7 +1323,7 @@ if there is no `if:` statement that limits the job to branch or merge request pi ...@@ -1321,7 +1323,7 @@ if there is no `if:` statement that limits the job to branch or merge request pi
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34272) in GitLab 13.6. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34272) in GitLab 13.6.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/267192) in GitLab 13.7. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/267192) in GitLab 13.7.
CI/CD variables can be used in `rules:changes` expressions to determine when You can use CI/CD variables in `rules:changes` expressions to determine when
to add jobs to a pipeline: to add jobs to a pipeline:
```yaml ```yaml
...@@ -1334,7 +1336,7 @@ docker build: ...@@ -1334,7 +1336,7 @@ docker build:
- $DOCKERFILES_DIR/* - $DOCKERFILES_DIR/*
``` ```
You can use The `$` character for both variables and paths. For example, if the You can use the `$` character for both variables and paths. For example, if the
`$DOCKERFILES_DIR` variable exists, its value is used. If it does not exist, the `$DOCKERFILES_DIR` variable exists, its value is used. If it does not exist, the
`$` is interpreted as being part of a path. `$` is interpreted as being part of a path.
...@@ -1342,10 +1344,10 @@ You can use The `$` character for both variables and paths. For example, if the ...@@ -1342,10 +1344,10 @@ You can use The `$` character for both variables and paths. For example, if the
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4.
`exists` accepts an array of paths and matches if any of these paths exist Use `exists` to run a job when certain files exist in the repository.
as files in the repository. You can use an array of paths.
In this example, `job` runs if a `Dockerfile` exists anywhere in the repository: In the following example, `job` runs if a `Dockerfile` exists anywhere in the repository:
```yaml ```yaml
job: job:
...@@ -1378,11 +1380,11 @@ For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns. A ...@@ -1378,11 +1380,11 @@ For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns. A
You can use [`allow_failure: true`](#allow_failure) in `rules:` to allow a job to fail, or a manual job to You can use [`allow_failure: true`](#allow_failure) in `rules:` to allow a job to fail, or a manual job to
wait for action, without stopping the pipeline itself. All jobs that use `rules:` default to `allow_failure: false` wait for action, without stopping the pipeline itself. All jobs that use `rules:` default to `allow_failure: false`
if `allow_failure:` is not defined. if you do not define `allow_failure:`.
The rule-level `rules:allow_failure` option overrides the job-level The rule-level `rules:allow_failure` option overrides the job-level
[`allow_failure`](#allow_failure) option, and is only applied when the job is [`allow_failure`](#allow_failure) option, and is only applied when
triggered by the particular rule. the particular rule triggers the job.
```yaml ```yaml
job: job:
...@@ -1400,7 +1402,7 @@ In this example, if the first rule matches, then the job has `when: manual` and ...@@ -1400,7 +1402,7 @@ In this example, if the first rule matches, then the job has `when: manual` and
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209864) in GitLab 13.7. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209864) in GitLab 13.7.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/289803) in GitLab 13.10. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/289803) in GitLab 13.10.
You can use [`variables`](#variables) in `rules:` to define variables for specific conditions. Use [`variables`](#variables) in `rules:` to define variables for specific conditions.
For example: For example:
...@@ -1476,14 +1478,14 @@ to add jobs to pipelines, use [`rules`](#rules). ...@@ -1476,14 +1478,14 @@ to add jobs to pipelines, use [`rules`](#rules).
1. `except` defines the names of branches and tags the job does 1. `except` defines the names of branches and tags the job does
**not** run for. **not** run for.
There are a few rules that apply to the usage of job policy: A few rules apply to the usage of job policy:
- `only` and `except` are inclusive. If both `only` and `except` are defined - `only` and `except` are inclusive. If both `only` and `except` are defined
in a job specification, the ref is filtered by `only` and `except`. in a job specification, the ref is filtered by `only` and `except`.
- `only` and `except` can use regular expressions ([supported regexp syntax](#supported-onlyexcept-regexp-syntax)). - `only` and `except` can use regular expressions ([supported regexp syntax](#supported-onlyexcept-regexp-syntax)).
- `only` and `except` can specify a repository path to filter jobs for forks. - `only` and `except` can specify a repository path to filter jobs for forks.
In addition, `only` and `except` can use special keywords: In addition, `only` and `except` can use these keywords:
| **Value** | **Description** | | **Value** | **Description** |
|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
...@@ -1504,8 +1506,8 @@ Scheduled pipelines run on specific branches, so jobs configured with `only: bra ...@@ -1504,8 +1506,8 @@ Scheduled pipelines run on specific branches, so jobs configured with `only: bra
run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches` run on scheduled pipelines too. Add `except: schedules` to prevent jobs with `only: branches`
from running on scheduled pipelines. from running on scheduled pipelines.
In the example below, `job` runs only for refs that start with `issue-`, In the following example, `job` runs only for refs that start with `issue-`.
whereas all branches are skipped: All branches are skipped:
```yaml ```yaml
job: job:
...@@ -1517,8 +1519,8 @@ job: ...@@ -1517,8 +1519,8 @@ job:
- branches - branches
``` ```
Pattern matching is case-sensitive by default. Use `i` flag modifier, like Pattern matching is case-sensitive by default. Use the `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive: `/pattern/i`, to make a pattern case-insensitive:
```yaml ```yaml
job: job:
...@@ -1530,8 +1532,8 @@ job: ...@@ -1530,8 +1532,8 @@ job:
- branches - branches
``` ```
In this example, `job` runs only for refs that are tagged, or if a build is In the following example, `job` runs only for refs that are tagged, or if a build is
explicitly requested by an API trigger or a [Pipeline Schedule](../pipelines/schedules.md): explicitly requested by an API trigger or a [pipeline schedule](../pipelines/schedules.md):
```yaml ```yaml
job: job:
...@@ -1542,8 +1544,7 @@ job: ...@@ -1542,8 +1544,7 @@ job:
- schedules - schedules
``` ```
Use the repository path to have jobs executed only for the parent To execute jobs only for the parent repository and not forks:
repository and not forks:
```yaml ```yaml
job: job:
...@@ -1554,11 +1555,11 @@ job: ...@@ -1554,11 +1555,11 @@ job:
- /^release/.*$/@gitlab-org/gitlab - /^release/.*$/@gitlab-org/gitlab
``` ```
The above example runs `job` for all branches on `gitlab-org/gitlab`, This example runs `job` for all branches on `gitlab-org/gitlab`,
except `master` and those with names prefixed with `release/`. except `master` and branches that start with `release/`.
If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by
default. If it does not have an `except` rule, it's empty. default. If the job does not have an `except` rule, it's empty.
For example, `job1` and `job2` are essentially the same: For example, `job1` and `job2` are essentially the same:
...@@ -1634,7 +1635,7 @@ the pipeline if the following is true: ...@@ -1634,7 +1635,7 @@ the pipeline if the following is true:
- `(any listed refs are true) AND (any listed variables are true) AND (any listed changes are true) AND (any chosen Kubernetes status matches)` - `(any listed refs are true) AND (any listed variables are true) AND (any listed changes are true) AND (any chosen Kubernetes status matches)`
In the example below, the `test` job is `only` created when **all** of the following are true: In the following example, the `test` job is `only` created when **all** of the following are true:
- The pipeline is [scheduled](../pipelines/schedules.md) **or** runs for `master`. - The pipeline is [scheduled](../pipelines/schedules.md) **or** runs for `master`.
- The `variables` keyword matches. - The `variables` keyword matches.
...@@ -1657,7 +1658,7 @@ added if the following is true: ...@@ -1657,7 +1658,7 @@ added if the following is true:
- `(any listed refs are true) OR (any listed variables are true) OR (any listed changes are true) OR (a chosen Kubernetes status matches)` - `(any listed refs are true) OR (any listed variables are true) OR (any listed changes are true) OR (a chosen Kubernetes status matches)`
In the example below, the `test` job is **not** created when **any** of the following are true: In the following example, the `test` job is **not** created when **any** of the following are true:
- The pipeline runs for the `master` branch. - The pipeline runs for the `master` branch.
- There are changes to the `README.md` file in the root directory of the repository. - There are changes to the `README.md` file in the root directory of the repository.
...@@ -1679,7 +1680,7 @@ test: ...@@ -1679,7 +1680,7 @@ test:
The `refs` strategy can take the same values as the The `refs` strategy can take the same values as the
[simplified only/except configuration](#onlyexcept-basic). [simplified only/except configuration](#onlyexcept-basic).
In the example below, the `deploy` job is created only when the In the following example, the `deploy` job is created only when the
pipeline is [scheduled](../pipelines/schedules.md) or runs for the `master` branch: pipeline is [scheduled](../pipelines/schedules.md) or runs for the `master` branch:
```yaml ```yaml
...@@ -1696,7 +1697,7 @@ deploy: ...@@ -1696,7 +1697,7 @@ deploy:
The `kubernetes` strategy accepts only the `active` keyword. The `kubernetes` strategy accepts only the `active` keyword.
In the example below, the `deploy` job is created only when the In the following example, the `deploy` job is created only when the
Kubernetes service is active in the project: Kubernetes service is active in the project:
```yaml ```yaml
...@@ -1767,7 +1768,7 @@ In pipelines with [sources other than the three above](../variables/predefined_v ...@@ -1767,7 +1768,7 @@ In pipelines with [sources other than the three above](../variables/predefined_v
You can configure jobs to use `only: changes` with other `only: refs` keywords. However, You can configure jobs to use `only: changes` with other `only: refs` keywords. However,
those jobs ignore the changes and always run. those jobs ignore the changes and always run.
In this example, when you push commits to an existing branch, the `docker build` job In the following example, when you push commits to an existing branch, the `docker build` job
runs only if any of these files change: runs only if any of these files change:
- The `Dockerfile` file. - The `Dockerfile` file.
...@@ -1864,7 +1865,7 @@ docker build service one: ...@@ -1864,7 +1865,7 @@ docker build service one:
- service-one/**/* - service-one/**/*
``` ```
In the example above, the pipeline might fail because of changes to a file in `service-one/**/*`. In this example, the pipeline might fail because of changes to a file in `service-one/**/*`.
A later commit that doesn't have changes in `service-one/**/*` A later commit that doesn't have changes in `service-one/**/*`
but does have changes to the `Dockerfile` can pass. The job but does have changes to the `Dockerfile` can pass. The job
...@@ -1898,13 +1899,22 @@ All files are considered to have changed when a scheduled pipeline runs. ...@@ -1898,13 +1899,22 @@ All files are considered to have changed when a scheduled pipeline runs.
> - In GitLab 12.3, maximum number of jobs in `needs` array raised from five to 50. > - In GitLab 12.3, maximum number of jobs in `needs` array raised from five to 50.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30631) in GitLab 12.8, `needs: []` lets jobs start immediately. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30631) in GitLab 12.8, `needs: []` lets jobs start immediately.
Use the `needs:` keyword to execute jobs out-of-order. Relationships between jobs Use `needs:` to execute jobs out-of-order. Relationships between jobs
that use `needs` can be visualized as a [directed acyclic graph](../directed_acyclic_graph/index.md). that use `needs` can be visualized as a [directed acyclic graph](../directed_acyclic_graph/index.md).
You can ignore stage ordering and run some jobs without waiting for others to complete. You can ignore stage ordering and run some jobs without waiting for others to complete.
Jobs in multiple stages can run concurrently. Jobs in multiple stages can run concurrently.
Let's consider the following example: The following example creates four paths of execution:
- Linter: the `lint` job runs immediately without waiting for the `build` stage
to complete because it has no needs (`needs: []`).
- Linux path: the `linux:rspec` and `linux:rubocop` jobs runs as soon as the `linux:build`
job finishes without waiting for `mac:build` to finish.
- macOS path: the `mac:rspec` and `mac:rubocop` jobs runs as soon as the `mac:build`
job finishes, without waiting for `linux:build` to finish.
- The `production` job runs as soon as all previous jobs finish; in this case:
`linux:build`, `linux:rspec`, `linux:rubocop`, `mac:build`, `mac:rspec`, `mac:rubocop`.
```yaml ```yaml
linux:build: linux:build:
...@@ -1937,20 +1947,6 @@ production: ...@@ -1937,20 +1947,6 @@ production:
stage: deploy stage: deploy
``` ```
This example creates four paths of execution:
- Linter: the `lint` job runs immediately without waiting for the `build` stage to complete because it has no needs (`needs: []`).
- Linux path: the `linux:rspec` and `linux:rubocop` jobs runs as soon
as the `linux:build` job finishes without waiting for `mac:build` to finish.
- macOS path: the `mac:rspec` and `mac:rubocop` jobs runs as soon
as the `mac:build` job finishes, without waiting for `linux:build` to finish.
- The `production` job runs as soon as all previous jobs
finish; in this case: `linux:build`, `linux:rspec`, `linux:rubocop`,
`mac:build`, `mac:rspec`, `mac:rubocop`.
#### Requirements and limitations #### Requirements and limitations
- If `needs:` is set to point to a job that is not instantiated - If `needs:` is set to point to a job that is not instantiated
...@@ -1967,7 +1963,7 @@ This example creates four paths of execution: ...@@ -1967,7 +1963,7 @@ This example creates four paths of execution:
- `needs:` is similar to `dependencies:` in that it must use jobs from prior stages, - `needs:` is similar to `dependencies:` in that it must use jobs from prior stages,
meaning it's impossible to create circular dependencies. Depending on jobs in the meaning it's impossible to create circular dependencies. Depending on jobs in the
current stage is not possible either, but support [is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/30632). current stage is not possible either, but support [is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/30632).
- Related to the above, stages must be explicitly defined for all jobs - Stages must be explicitly defined for all jobs
that have the keyword `needs:` or are referred to by one. that have the keyword `needs:` or are referred to by one.
##### Changing the `needs:` job limit **(FREE SELF)** ##### Changing the `needs:` job limit **(FREE SELF)**
...@@ -1990,7 +1986,7 @@ To disable directed acyclic graphs (DAG), set the limit to `0`. ...@@ -1990,7 +1986,7 @@ To disable directed acyclic graphs (DAG), set the limit to `0`.
Use `artifacts: true` (default) or `artifacts: false` to control when artifacts are Use `artifacts: true` (default) or `artifacts: false` to control when artifacts are
downloaded in jobs that use `needs`. downloaded in jobs that use `needs`.
In this example, the `rspec` job downloads the `build_job` artifacts, but the In the following example, the `rspec` job downloads the `build_job` artifacts, but the
`rubocop` job does not: `rubocop` job does not:
```yaml ```yaml
...@@ -2013,7 +2009,7 @@ rubocop: ...@@ -2013,7 +2009,7 @@ rubocop:
artifacts: false artifacts: false
``` ```
In this example, the `rspec` job downloads the artifacts from all three `build_jobs`. In the following example, the `rspec` job downloads the artifacts from all three `build_jobs`.
`artifacts` is: `artifacts` is:
- Set to true for `build_job_1`. - Set to true for `build_job_1`.
...@@ -2064,7 +2060,7 @@ The user running the pipeline must have at least `reporter` access to the group ...@@ -2064,7 +2060,7 @@ The user running the pipeline must have at least `reporter` access to the group
Use `needs` to download artifacts from different pipelines in the current project. Use `needs` to download artifacts from different pipelines in the current project.
Set the `project` keyword as the current project's name, and specify a ref. Set the `project` keyword as the current project's name, and specify a ref.
In this example, `build_job` downloads the artifacts for the latest successful In the following example, `build_job` downloads the artifacts for the latest successful
`build-1` job with the `other-ref` ref: `build-1` job with the `other-ref` ref:
```yaml ```yaml
...@@ -2153,7 +2149,7 @@ available for the project. ...@@ -2153,7 +2149,7 @@ available for the project.
When you register a runner, you can specify the runner's tags, for When you register a runner, you can specify the runner's tags, for
example `ruby`, `postgres`, `development`. example `ruby`, `postgres`, `development`.
In this example, the job is run by a runner that In the following example, the job is run by a runner that
has both `ruby` and `postgres` tags defined. has both `ruby` and `postgres` tags defined.
```yaml ```yaml
...@@ -2202,7 +2198,7 @@ Assuming all other jobs are successful, the job's stage and its pipeline ...@@ -2202,7 +2198,7 @@ Assuming all other jobs are successful, the job's stage and its pipeline
show the same orange warning. However, the associated commit is marked as show the same orange warning. However, the associated commit is marked as
"passed", without warnings. "passed", without warnings.
In the example below, `job1` and `job2` run in parallel, but if `job1` In the following example, `job1` and `job2` run in parallel. If `job1`
fails, it doesn't stop the next stage from running, because it's marked with fails, it doesn't stop the next stage from running, because it's marked with
`allow_failure: true`: `allow_failure: true`:
...@@ -2269,7 +2265,12 @@ The valid values of `when` are: ...@@ -2269,7 +2265,12 @@ The valid values of `when` are:
- With [`rules`](#rules), don't execute job. - With [`rules`](#rules), don't execute job.
- With [`workflow`](#workflow), don't run pipeline. - With [`workflow`](#workflow), don't run pipeline.
For example: In the following example, the script:
1. Executes `cleanup_build_job` only when `build_job` fails.
1. Always executes `cleanup_job` as the last step in pipeline regardless of
success or failure.
1. Executes `deploy_job` when you run it manually in the GitLab UI.
```yaml ```yaml
stages: stages:
...@@ -2308,13 +2309,6 @@ cleanup_job: ...@@ -2308,13 +2309,6 @@ cleanup_job:
when: always when: always
``` ```
The above script:
1. Executes `cleanup_build_job` only when `build_job` fails.
1. Always executes `cleanup_job` as the last step in pipeline regardless of
success or failure.
1. Executes `deploy_job` when you run it manually in the GitLab UI.
#### `when:manual` #### `when:manual`
A manual job is a type of job that is not executed automatically and must be explicitly A manual job is a type of job that is not executed automatically and must be explicitly
...@@ -2377,7 +2371,7 @@ To protect a manual job: ...@@ -2377,7 +2371,7 @@ To protect a manual job:
``` ```
1. In the [protected environments settings](../environments/protected_environments.md#protecting-environments), 1. In the [protected environments settings](../environments/protected_environments.md#protecting-environments),
select the environment (`production` in the example above) and add the users, roles or groups select the environment (`production` in this example) and add the users, roles or groups
that are authorized to trigger the manual job to the **Allowed to Deploy** list. Only those in that are authorized to trigger the manual job to the **Allowed to Deploy** list. Only those in
this list can trigger this manual job, as well as GitLab administrators this list can trigger this manual job, as well as GitLab administrators
who are always able to use protected environments. who are always able to use protected environments.
...@@ -2670,7 +2664,7 @@ as Review Apps. You can see an example that uses Review Apps at ...@@ -2670,7 +2664,7 @@ as Review Apps. You can see an example that uses Review Apps at
### `cache` ### `cache`
Use the `cache` keyword to specify a list of files and directories to Use `cache` to specify a list of files and directories to
cache between jobs. You can only use paths that are in the local working copy. cache between jobs. You can only use paths that are in the local working copy.
If `cache` is defined outside the scope of jobs, it's set If `cache` is defined outside the scope of jobs, it's set
...@@ -2771,7 +2765,7 @@ to download cache that's tagged with `test`. ...@@ -2771,7 +2765,7 @@ to download cache that's tagged with `test`.
If a cache with this tag is not found, you can use `CACHE_FALLBACK_KEY` to If a cache with this tag is not found, you can use `CACHE_FALLBACK_KEY` to
specify a cache to use when none exists. specify a cache to use when none exists.
In this example, if the `$CI_COMMIT_REF_SLUG` is not found, the job uses the key defined In the following example, if the `$CI_COMMIT_REF_SLUG` is not found, the job uses the key defined
by the `CACHE_FALLBACK_KEY` variable: by the `CACHE_FALLBACK_KEY` variable:
```yaml ```yaml
...@@ -2808,7 +2802,7 @@ cache: ...@@ -2808,7 +2802,7 @@ cache:
- node_modules - node_modules
``` ```
In this example we're creating a cache for Ruby and Node.js dependencies that This example creates a cache for Ruby and Node.js dependencies that
is tied to current versions of the `Gemfile.lock` and `package.json` files. Whenever one of is tied to current versions of the `Gemfile.lock` and `package.json` files. Whenever one of
these files changes, a new cache key is computed and a new cache is created. Any future these files changes, a new cache key is computed and a new cache is created. Any future
job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files` job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files`
...@@ -2942,7 +2936,7 @@ To do so, add `policy: push` to the job. ...@@ -2942,7 +2936,7 @@ To do so, add `policy: push` to the job.
### `artifacts` ### `artifacts`
Use the `artifacts` keyword to specify a list of files and directories that are Use `artifacts` to specify a list of files and directories that are
attached to the job when it [succeeds, fails, or always](#artifactswhen). attached to the job when it [succeeds, fails, or always](#artifactswhen).
The artifacts are sent to GitLab after the job finishes. They are The artifacts are sent to GitLab after the job finishes. They are
...@@ -3422,7 +3416,7 @@ If `retry` is set to `2`, and a job succeeds in a second run (first retry), it i ...@@ -3422,7 +3416,7 @@ If `retry` is set to `2`, and a job succeeds in a second run (first retry), it i
The `retry` value must be a positive integer, from `0` to `2` The `retry` value must be a positive integer, from `0` to `2`
(two retries maximum, three runs in total). (two retries maximum, three runs in total).
This example retries all failure cases: The following example retries all failure cases:
```yaml ```yaml
test: test:
...@@ -3584,7 +3578,7 @@ deploystacks: ...@@ -3584,7 +3578,7 @@ deploystacks:
STACK: [data, processing] STACK: [data, processing]
``` ```
This example generates 10 parallel `deploystacks` jobs, each with different values The following example generates 10 parallel `deploystacks` jobs, each with different values
for `PROVIDER` and `STACK`: for `PROVIDER` and `STACK`:
```plaintext ```plaintext
...@@ -3769,6 +3763,10 @@ To force the `trigger` job to wait for the downstream (multi-project or child) p ...@@ -3769,6 +3763,10 @@ To force the `trigger` job to wait for the downstream (multi-project or child) p
pipeline completes. At that point, the `trigger` job completes and displays the same status as pipeline completes. At that point, the `trigger` job completes and displays the same status as
the downstream job. the downstream job.
This setting can help keep your pipeline execution linear. In the following example, jobs from
subsequent stages wait for the triggered pipeline to successfully complete before
starting, which reduces parallelization.
```yaml ```yaml
trigger_job: trigger_job:
trigger: trigger:
...@@ -3776,10 +3774,6 @@ trigger_job: ...@@ -3776,10 +3774,6 @@ trigger_job:
strategy: depend strategy: depend
``` ```
This setting can help keep your pipeline execution linear. In the example above, jobs from
subsequent stages wait for the triggered pipeline to successfully complete before
starting, which reduces parallelization.
#### Trigger a pipeline by API call #### Trigger a pipeline by API call
To force a rebuild of a specific branch, tag, or commit, you can use an API call To force a rebuild of a specific branch, tag, or commit, you can use an API call
...@@ -3806,7 +3800,12 @@ When enabled, a pipeline is immediately canceled when a new pipeline starts on t ...@@ -3806,7 +3800,12 @@ When enabled, a pipeline is immediately canceled when a new pipeline starts on t
Set jobs as interruptible that can be safely canceled once started (for instance, a build job). Set jobs as interruptible that can be safely canceled once started (for instance, a build job).
For example: In the following example, a new pipeline run causes an existing running pipeline to be:
- Canceled, if only `step-1` is running or pending.
- Not canceled, once `step-2` starts running.
After an uninterruptible job starts running, the pipeline cannot be canceled.
```yaml ```yaml
stages: stages:
...@@ -3832,13 +3831,6 @@ step-3: ...@@ -3832,13 +3831,6 @@ step-3:
interruptible: true interruptible: true
``` ```
In the example above, a new pipeline run causes an existing running pipeline to be:
- Canceled, if only `step-1` is running or pending.
- Not canceled, once `step-2` starts running.
When an uninterruptible job is running, the pipeline cannot be canceled, regardless of the final job's state.
### `resource_group` ### `resource_group`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15536) in GitLab 12.7. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15536) in GitLab 12.7.
...@@ -3885,7 +3877,7 @@ executions. The [`trigger` keyword](#trigger) can trigger downstream pipelines. ...@@ -3885,7 +3877,7 @@ executions. The [`trigger` keyword](#trigger) can trigger downstream pipelines.
[`resource_group` keyword](#resource_group) can co-exist with it. This is useful to control the [`resource_group` keyword](#resource_group) can co-exist with it. This is useful to control the
concurrency for deployment pipelines, while running non-sensitive jobs concurrently. concurrency for deployment pipelines, while running non-sensitive jobs concurrently.
This example has two pipeline configurations in a project. When a pipeline starts running, The following example has two pipeline configurations in a project. When a pipeline starts running,
non-sensitive jobs are executed first and aren't affected by concurrent executions in other non-sensitive jobs are executed first and aren't affected by concurrent executions in other
pipelines. However, GitLab ensures that there are no other deployment pipelines running before pipelines. However, GitLab ensures that there are no other deployment pipelines running before
triggering a deployment (child) pipeline. If other deployment pipelines are running, GitLab waits triggering a deployment (child) pipeline. If other deployment pipelines are running, GitLab waits
...@@ -3926,7 +3918,7 @@ deployment: ...@@ -3926,7 +3918,7 @@ deployment:
script: echo "Deploying..." script: echo "Deploying..."
``` ```
Note that you must define [`strategy: depend`](#linking-pipelines-with-triggerstrategy) You must define [`strategy: depend`](#linking-pipelines-with-triggerstrategy)
with the `trigger` keyword. This ensures that the lock isn't released until the downstream pipeline with the `trigger` keyword. This ensures that the lock isn't released until the downstream pipeline
finishes. finishes.
...@@ -3934,7 +3926,7 @@ finishes. ...@@ -3934,7 +3926,7 @@ finishes.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/19298) in GitLab 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/19298) in GitLab 13.2.
`release` indicates that the job creates a [Release](../../user/project/releases/index.md). Use `release` to create a [release](../../user/project/releases/index.md).
These keywords are supported: These keywords are supported:
...@@ -3945,18 +3937,18 @@ These keywords are supported: ...@@ -3945,18 +3937,18 @@ These keywords are supported:
- [`milestones`](#releasemilestones) (optional) - [`milestones`](#releasemilestones) (optional)
- [`released_at`](#releasereleased_at) (optional) - [`released_at`](#releasereleased_at) (optional)
The Release is created only if the job processes without error. If the Rails API The release is created only if the job processes without error. If the Rails API
returns an error during Release creation, the `release` job fails. returns an error during release creation, the `release` job fails.
#### `release-cli` Docker image #### `release-cli` Docker image
The Docker image to use for the `release-cli` must be specified, using the following directive: You must specify the Docker image to use for the `release-cli`:
```yaml ```yaml
image: registry.gitlab.com/gitlab-org/release-cli:latest image: registry.gitlab.com/gitlab-org/release-cli:latest
``` ```
#### Script #### `script`
All jobs except [trigger](#trigger) jobs must have the `script` keyword. A `release` All jobs except [trigger](#trigger) jobs must have the `script` keyword. A `release`
job can use the output from script commands, but you can use a placeholder script if job can use the output from script commands, but you can use a placeholder script if
...@@ -3989,11 +3981,12 @@ android-release: ...@@ -3989,11 +3981,12 @@ android-release:
#### `release:tag_name` #### `release:tag_name`
The `tag_name` must be specified. It can refer to an existing Git tag or can be specified by the user. You must specify a `tag_name` for the release. The tag can refer to an existing Git tag or
you can specify a new tag.
When the specified tag doesn't exist in the repository, a new tag is created from the associated SHA of the pipeline. When the specified tag doesn't exist in the repository, a new tag is created from the associated SHA of the pipeline.
For example, when creating a Release from a Git tag: For example, when creating a release from a Git tag:
```yaml ```yaml
job: job:
...@@ -4012,17 +4005,17 @@ job: ...@@ -4012,17 +4005,17 @@ job:
description: 'Release description' description: 'Release description'
``` ```
- The Release is created only if the job's main script succeeds. - The release is created only if the job's main script succeeds.
- If the Release already exists, it is not updated and the job with the `release` keyword fails. - If the release already exists, it is not updated and the job with the `release` keyword fails.
- The `release` section executes after the `script` tag and before the `after_script`. - The `release` section executes after the `script` tag and before the `after_script`.
#### `release:name` #### `release:name`
The Release name. If omitted, it is populated with the value of `release: tag_name`. The release name. If omitted, it is populated with the value of `release: tag_name`.
#### `release:description` #### `release:description`
Specifies the long description of the Release. You can also specify a file that contains the Specifies the long description of the release. You can also specify a file that contains the
description. description.
##### Read description from a file ##### Read description from a file
...@@ -4060,8 +4053,7 @@ released_at: '2021-03-15T08:00:00Z' ...@@ -4060,8 +4053,7 @@ released_at: '2021-03-15T08:00:00Z'
#### Complete example for `release` #### Complete example for `release`
Combining the individual examples given above for `release` results in the following If you combine the previous examples for `release`, you get two options, depending on how you generate the
code snippets. There are two options, depending on how you generate the
tags. You can't use these options together, so choose one: tags. You can't use these options together, so choose one:
- To create a release when you push a Git tag, or when you add a Git tag - To create a release when you push a Git tag, or when you add a Git tag
...@@ -4145,7 +4137,7 @@ The entries under the `release` node are transformed into a `bash` command line ...@@ -4145,7 +4137,7 @@ The entries under the `release` node are transformed into a `bash` command line
to the Docker container, which contains the [release-cli](https://gitlab.com/gitlab-org/release-cli). to the Docker container, which contains the [release-cli](https://gitlab.com/gitlab-org/release-cli).
You can also call the `release-cli` directly from a `script` entry. You can also call the `release-cli` directly from a `script` entry.
For example, using the YAML described above: For example, if you use the YAML described previously:
```shell ```shell
release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "v${MAJOR}.${MINOR}.${REVISION}" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "v${MAJOR}.${MINOR}.${REVISION}" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3"
...@@ -4155,7 +4147,7 @@ release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using ...@@ -4155,7 +4147,7 @@ release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33014) in GitLab 13.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33014) in GitLab 13.4.
`secrets` indicates the [CI/CD Secrets](../secrets/index.md) this job needs. It should be a hash, Use `secrets` to specify the [CI/CD Secrets](../secrets/index.md) the job needs. It should be a hash,
and the keys should be the names of the variables that are made available to the job. and the keys should be the names of the variables that are made available to the job.
The value of each secret is saved in a temporary file. This file's path is stored in these The value of each secret is saved in a temporary file. This file's path is stored in these
variables. variables.
...@@ -4164,7 +4156,8 @@ variables. ...@@ -4164,7 +4156,8 @@ variables.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4.
`vault` keyword specifies secrets provided by [Hashicorp's Vault](https://www.vaultproject.io/). Use `vault` to specify secrets provided by [Hashicorp's Vault](https://www.vaultproject.io/).
This syntax has multiple forms. The shortest form assumes the use of the This syntax has multiple forms. The shortest form assumes the use of the
[KV-V2](https://www.vaultproject.io/docs/secrets/kv/kv-v2) secrets engine, [KV-V2](https://www.vaultproject.io/docs/secrets/kv/kv-v2) secrets engine,
mounted at the default path `kv-v2`. The last part of the secret's path is the mounted at the default path `kv-v2`. The last part of the secret's path is the
...@@ -4202,14 +4195,13 @@ job: ...@@ -4202,14 +4195,13 @@ job:
### `pages` ### `pages`
`pages` is a special job that uploads static content to GitLab that Use `pages` to upload static content to GitLab. The content
is then published as a website. It has a special syntax, so the two is then published as a website. You must:
requirements below must be met:
- Any static content must be placed under a `public/` directory. - Place any static content in a `public/` directory.
- `artifacts` with a path to the `public/` directory must be defined. - Define [`artifacts`](#artifacts) with a path to the `public/` directory.
The example below moves all files from the root of the project to the The following example moves all files from the root of the project to the
`public/` directory. The `.public` workaround is so `cp` does not also copy `public/` directory. The `.public` workaround is so `cp` does not also copy
`public/` to itself in an infinite loop: `public/` to itself in an infinite loop:
...@@ -4227,14 +4219,14 @@ pages: ...@@ -4227,14 +4219,14 @@ pages:
- master - master
``` ```
Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). View the [GitLab Pages user documentation](../../user/project/pages/index.md).
### `inherit` ### `inherit`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9.
You can disable inheritance of globally defined defaults Use `inherit:` to control inheritance of globally-defined defaults
and variables with the `inherit:` keyword. and variables.
To enable or disable the inheritance of all `default:` or `variables:` keywords, use: To enable or disable the inheritance of all `default:` or `variables:` keywords, use:
...@@ -4263,7 +4255,7 @@ inherit: ...@@ -4263,7 +4255,7 @@ inherit:
- VARIABLE2 - VARIABLE2
``` ```
In the example below: In the following example:
- `rubocop`: - `rubocop`:
- inherits: Nothing. - inherits: Nothing.
...@@ -4367,7 +4359,7 @@ You can use [YAML anchors for variables](#yaml-anchors-for-variables). ...@@ -4367,7 +4359,7 @@ You can use [YAML anchors for variables](#yaml-anchors-for-variables).
> [Introduced in](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) GitLab 13.7. > [Introduced in](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) GitLab 13.7.
You can use the `value` and `description` keywords to define [variables that are prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines) Use the `value` and `description` keywords to define [variables that are prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines)
when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually): when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually):
```yaml ```yaml
...@@ -4379,7 +4371,7 @@ variables: ...@@ -4379,7 +4371,7 @@ variables:
### Configure runner behavior with variables ### Configure runner behavior with variables
You can use [CI/CD variables](../variables/README.md) to configure runner Git behavior: You can use [CI/CD variables](../variables/README.md) to configure how the runner processes Git requests:
- [`GIT_STRATEGY`](../runners/README.md#git-strategy) - [`GIT_STRATEGY`](../runners/README.md#git-strategy)
- [`GIT_SUBMODULE_STRATEGY`](../runners/README.md#git-submodule-strategy) - [`GIT_SUBMODULE_STRATEGY`](../runners/README.md#git-submodule-strategy)
...@@ -4395,16 +4387,16 @@ You can use [CI/CD variables](../variables/README.md) to configure runner Git be ...@@ -4395,16 +4387,16 @@ You can use [CI/CD variables](../variables/README.md) to configure runner Git be
You can also use variables to configure how many times a runner You can also use variables to configure how many times a runner
[attempts certain stages of job execution](../runners/README.md#job-stages-attempts). [attempts certain stages of job execution](../runners/README.md#job-stages-attempts).
## Special YAML features ## YAML-specific features
It's possible to use special YAML features like anchors (`&`), aliases (`*`) In your `.gitlab-ci.yml` file, you can use YAML-specific features like anchors (`&`), aliases (`*`),
and map merging (`<<`). Use these features to reduce the complexity and map merging (`<<`). Use these features to reduce the complexity
of the code in the `.gitlab-ci.yml` file. of the code in the `.gitlab-ci.yml` file.
Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/). Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
In most cases, the [`extends` keyword](#extends) is more user friendly and should In most cases, the [`extends` keyword](#extends) is more user friendly and you should
be used over these special YAML features. use it when possible.
You can use YAML anchors to merge YAML arrays. You can use YAML anchors to merge YAML arrays.
...@@ -4445,8 +4437,8 @@ test2: ...@@ -4445,8 +4437,8 @@ test2:
``` ```
`&` sets up the name of the anchor (`job_configuration`), `<<` means "merge the `&` sets up the name of the anchor (`job_configuration`), `<<` means "merge the
given hash into the current one", and `*` includes the named anchor given hash into the current one," and `*` includes the named anchor
(`job_configuration` again). The expanded version of the example above is: (`job_configuration` again). The expanded version of this example is:
```yaml ```yaml
.job_template: .job_template:
...@@ -4586,7 +4578,7 @@ Use [YAML anchors](#anchors) with `variables` to repeat assignment ...@@ -4586,7 +4578,7 @@ Use [YAML anchors](#anchors) with `variables` to repeat assignment
of variables across multiple jobs. You can also use YAML anchors when a job of variables across multiple jobs. You can also use YAML anchors when a job
requires a specific `variables` block that would otherwise override the global variables. requires a specific `variables` block that would otherwise override the global variables.
In the example below, we override the `GIT_STRATEGY` variable without affecting The following example shows how override the `GIT_STRATEGY` variable without affecting
the use of the `SAMPLE_VARIABLE` variable: the use of the `SAMPLE_VARIABLE` variable:
```yaml ```yaml
...@@ -4606,7 +4598,7 @@ job_no_git_strategy: ...@@ -4606,7 +4598,7 @@ job_no_git_strategy:
### Hide jobs ### Hide jobs
If you want to temporarily 'disable' a job, rather than commenting out all the If you want to temporarily disable a job, rather than commenting out all the
lines where the job is defined: lines where the job is defined:
```yaml ```yaml
...@@ -4625,7 +4617,7 @@ GitLab CI/CD. In the following example, `.hidden_job` is ignored: ...@@ -4625,7 +4617,7 @@ GitLab CI/CD. In the following example, `.hidden_job` is ignored:
``` ```
Use this feature to ignore jobs, or use the Use this feature to ignore jobs, or use the
[special YAML features](#special-yaml-features) and transform the hidden jobs [YAML-specific features](#yaml-specific-features) and transform the hidden jobs
into templates. into templates.
### `!reference` tags ### `!reference` tags
...@@ -4637,7 +4629,7 @@ sections and reuse it in the current section. Unlike [YAML anchors](#anchors), y ...@@ -4637,7 +4629,7 @@ sections and reuse it in the current section. Unlike [YAML anchors](#anchors), y
use `!reference` tags to reuse configuration from [included](#include) configuration use `!reference` tags to reuse configuration from [included](#include) configuration
files as well. files as well.
In this example, a `script` and an `after_script` from two different locations are In the following example, a `script` and an `after_script` from two different locations are
reused in the `test` job: reused in the `test` job:
- `setup.yml`: - `setup.yml`:
...@@ -4666,7 +4658,7 @@ reused in the `test` job: ...@@ -4666,7 +4658,7 @@ reused in the `test` job:
- !reference [.teardown, after_script] - !reference [.teardown, after_script]
``` ```
In this example, `test-vars-1` reuses the all the variables in `.vars`, while `test-vars-2` In the following example, `test-vars-1` reuses the all the variables in `.vars`, while `test-vars-2`
selects a specific variable and reuses it as a new `MY_VAR` variable. selects a specific variable and reuses it as a new `MY_VAR` variable.
```yaml ```yaml
......
import mountSubscriptionsApplication from 'ee/subscriptions/buy_minutes';
mountSubscriptionsApplication();
...@@ -20,7 +20,7 @@ import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue' ...@@ -20,7 +20,7 @@ import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale'; import { s__, __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants'; import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import IssuesBadge from './issues_badge.vue'; import IssuesBadge from './issues_badge.vue';
...@@ -230,7 +230,7 @@ export default { ...@@ -230,7 +230,7 @@ export default {
} }
if (file && startLine) { if (file && startLine) {
return `${file} ${sprintf(__('(line: %{startLine})'), { startLine })}`; return `${file}:${startLine}`;
} }
if (path) { if (path) {
...@@ -248,6 +248,16 @@ export default { ...@@ -248,6 +248,16 @@ export default {
extraIdentifierCount(identifiers) { extraIdentifierCount(identifiers) {
return identifiers?.length - 1; return identifiers?.length - 1;
}, },
fileUrl(vulnerability) {
const { startLine: start, endLine: end, blobPath } = vulnerability.location;
const lineNumber = end > start ? `${start}-${end}` : start;
if (!blobPath) {
return '';
}
return `${blobPath}${lineNumber ? `#L${lineNumber}` : ''}`;
},
primaryIdentifier(identifiers) { primaryIdentifier(identifiers) {
return getPrimaryIdentifier(identifiers, 'externalType'); return getPrimaryIdentifier(identifiers, 'externalType');
}, },
...@@ -421,8 +431,11 @@ export default { ...@@ -421,8 +431,11 @@ export default {
<div v-if="shouldShowProjectNamespace"> <div v-if="shouldShowProjectNamespace">
{{ item.project.nameWithNamespace }} {{ item.project.nameWithNamespace }}
</div> </div>
<div v-if="shouldShowVulnerabilityPath(item)" class="monospace"> <div v-if="shouldShowVulnerabilityPath(item)">
<gl-truncate :text="createLocationString(item.location)" position="middle" /> <gl-link v-if="item.location.blobPath" :href="fileUrl(item)">
<gl-truncate :text="createLocationString(item.location)" position="middle" />
</gl-link>
<gl-truncate v-else :text="createLocationString(item.location)" position="middle" />
</div> </div>
</div> </div>
</template> </template>
......
...@@ -27,13 +27,16 @@ fragment Vulnerability on Vulnerability { ...@@ -27,13 +27,16 @@ fragment Vulnerability on Vulnerability {
image image
} }
... on VulnerabilityLocationDependencyScanning { ... on VulnerabilityLocationDependencyScanning {
blobPath
file file
} }
... on VulnerabilityLocationSast { ... on VulnerabilityLocationSast {
blobPath
file file
startLine startLine
} }
... on VulnerabilityLocationSecretDetection { ... on VulnerabilityLocationSecretDetection {
blobPath
file file
startLine startLine
} }
......
<script>
import StepOrderApp from 'ee/vue_shared/components/step_order_app.vue';
export default {
components: {
StepOrderApp,
},
};
</script>
<template>
<step-order-app>
<template #checkout></template>
<template #order-summary></template>
</step-order-app>
</template>
import Vue from 'vue';
import App from './components/app.vue';
export default () => {
const el = document.getElementById('js-buy-minutes');
return new Vue({
el,
components: {
App,
},
render(createElement) {
return createElement(App);
},
});
};
<script>
import StepOrderApp from 'ee/vue_shared/components/step_order_app.vue';
import Checkout from './checkout.vue';
import OrderSummary from './order_summary.vue';
export default {
components: {
StepOrderApp,
Checkout,
OrderSummary,
},
};
</script>
<template>
<step-order-app>
<template #checkout>
<checkout />
</template>
<template #order-summary>
<order-summary />
</template>
</step-order-app>
</template>
import Vue from 'vue'; import Vue from 'vue';
import Checkout from './components/checkout.vue'; import App from './components/app.vue';
import OrderSummary from './components/order_summary.vue';
import createStore from './store'; import createStore from './store';
export default () => { export default () => {
const checkoutEl = document.getElementById('checkout'); const el = document.getElementById('js-new-subscription');
const summaryEl = document.getElementById('summary'); const store = createStore(el.dataset);
const store = createStore(checkoutEl.dataset);
// eslint-disable-next-line no-new
new Vue({
el: checkoutEl,
store,
render(createElement) {
return createElement(Checkout);
},
});
return new Vue({ return new Vue({
el: summaryEl, el,
store, store,
components: {
App,
},
render(createElement) { render(createElement) {
return createElement(OrderSummary); return createElement(App);
}, },
}); });
}; };
<template>
<div
class="row gl-flex-grow-1 gl-flex-direction-column flex-nowrap gl-lg-flex-direction-row flex-xl-row flex-lg-wrap flex-xl-wrap"
>
<div
class="checkout-pane gl-px-3 gl-align-items-center gl-bg-gray-10 col-lg-7 gl-display-flex gl-flex-direction-column gl-flex-grow-1"
>
<slot name="checkout"></slot>
</div>
<div
class="gl-pb-3 gl-px-3 px-lg-7 col-lg-5 gl-display-flex gl-flex-direction-row gl-justify-content-center"
>
<slot name="order-summary"></slot>
</div>
</div>
</template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
class SubscriptionsController < ApplicationController class SubscriptionsController < ApplicationController
layout 'checkout' layout 'checkout'
skip_before_action :authenticate_user!, only: :new skip_before_action :authenticate_user!, only: [:new, :buy_minutes]
feature_category :purchase feature_category :purchase
...@@ -24,10 +24,12 @@ class SubscriptionsController < ApplicationController ...@@ -24,10 +24,12 @@ class SubscriptionsController < ApplicationController
end end
def new def new
return if current_user redirect_unauthenticated_user('checkout')
end
store_location_for :user, request.fullpath def buy_minutes
redirect_to new_user_registration_path(redirect_from: 'checkout') render_404 unless Feature.enabled?(:new_route_ci_minutes_purchase, default_enabled: :yaml)
redirect_unauthenticated_user
end end
def payment_form def payment_form
...@@ -90,4 +92,11 @@ class SubscriptionsController < ApplicationController ...@@ -90,4 +92,11 @@ class SubscriptionsController < ApplicationController
def customer_portal_new_subscription_url def customer_portal_new_subscription_url
"#{EE::SUBSCRIPTIONS_URL}/subscriptions/new?plan_id=#{params[:plan_id]}&transaction=create_subscription" "#{EE::SUBSCRIPTIONS_URL}/subscriptions/new?plan_id=#{params[:plan_id]}&transaction=create_subscription"
end end
def redirect_unauthenticated_user(from = action_name)
return if current_user
store_location_for :user, request.fullpath
redirect_to new_user_registration_path(redirect_from: from)
end
end end
...@@ -107,7 +107,7 @@ module Security ...@@ -107,7 +107,7 @@ module Security
return by_find_params if by_find_params return by_find_params if by_find_params
Gitlab::ErrorTracking.track_and_raise_exception(e, find_params: find_params, uuid: finding.uuid) Gitlab::ErrorTracking.track_and_raise_exception(e, find_params: find_params, uuid: finding.uuid)
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::ActiveRecordError => e
Gitlab::ErrorTracking.track_and_raise_exception(e, create_params: create_params&.dig(:raw_metadata)) Gitlab::ErrorTracking.track_and_raise_exception(e, create_params: create_params&.dig(:raw_metadata))
end end
end end
......
- page_title _('Buy CI Minutes')
#js-buy-minutes{ data: subscription_data }
- page_title _('Checkout') - page_title _('Checkout')
.row.flex-grow-1.flex-column.flex-nowrap.flex-lg-row.flex-xl-row.flex-lg-wrap.flex-xl-wrap #js-new-subscription{ data: subscription_data }
.checkout-pane.px-3.align-items-center.bg-gray-light.col-lg-7.d-flex.flex-column.flex-grow-1
#checkout{ data: subscription_data }
.pb-3.px-3.px-lg-7.col-lg-5.d-flex.flex-row.justify-content-center
#summary
---
title: Make vulnerability file path linkable in the vulnerability list
merge_request: 55356
author:
type: changed
# frozen_string_literal: true # frozen_string_literal: true
resource :subscriptions, only: [:new, :create] do resource :subscriptions, only: [:new, :create] do
get :buy_minutes
get :payment_form get :payment_form
get :payment_method get :payment_method
......
...@@ -84,7 +84,7 @@ module Gitlab ...@@ -84,7 +84,7 @@ module Gitlab
# Returns true if load balancing is to be enabled. # Returns true if load balancing is to be enabled.
def self.enable? def self.enable?
return false if program_name == 'rake' || Gitlab::Runtime.sidekiq? return false if Gitlab::Runtime.rake? || Gitlab::Runtime.sidekiq?
return false unless self.configured? return false unless self.configured?
true true
......
...@@ -5,21 +5,39 @@ require 'spec_helper' ...@@ -5,21 +5,39 @@ require 'spec_helper'
RSpec.describe SubscriptionsController do RSpec.describe SubscriptionsController do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
describe 'GET #new' do shared_examples 'unauthenticated subscription request' do |redirect_from|
subject { get :new, params: { plan_id: 'bronze_id' } } it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to new_user_registration_path(redirect_from: redirect_from) }
context 'with unauthenticated user' do it 'stores subscription URL for later' do
it { is_expected.to have_gitlab_http_status(:redirect) } subject
it { is_expected.to redirect_to new_user_registration_path(redirect_from: 'checkout') }
it 'stores subscription URL for later' do expected_subscription_path = new_subscriptions_path(plan_id: 'bronze_id') if redirect_from == 'checkout'
subject expected_subscription_path = buy_minutes_subscriptions_path(plan_id: 'bronze_id') if redirect_from == 'buy_minutes'
expected_subscription_path = new_subscriptions_path(plan_id: 'bronze_id') expect(controller.stored_location_for(:user)).to eq(expected_subscription_path)
end
end
expect(controller.stored_location_for(:user)).to eq(expected_subscription_path) describe 'GET #new' do
subject { get :new, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'checkout'
context 'with authenticated user' do
before do
sign_in(user)
end end
it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :new }
end end
end
describe 'GET #buy_minutes' do
subject { get :buy_minutes, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'buy_minutes'
context 'with authenticated user' do context 'with authenticated user' do
before do before do
...@@ -27,7 +45,16 @@ RSpec.describe SubscriptionsController do ...@@ -27,7 +45,16 @@ RSpec.describe SubscriptionsController do
end end
it { is_expected.to render_template 'layouts/checkout' } it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :new } it { is_expected.to render_template :buy_minutes }
end
context 'with :new_route_ci_minutes_purchase disabled' do
before do
sign_in(user)
stub_feature_flags(new_route_ci_minutes_purchase: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end end
end end
......
...@@ -58,6 +58,8 @@ export const generateVulnerabilities = () => [ ...@@ -58,6 +58,8 @@ export const generateVulnerabilities = () => [
location: { location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java', file: 'src/main/java/com/gitlab/security_products/tests/App.java',
startLine: '1337', startLine: '1337',
blobPath:
'/gitlab-org/security-reports2/-/blob/e5c61e4d5d0b8418011171def04ca0aa36532621/src/main/java/com/gitlab/security_products/tests/App.java',
}, },
project: { project: {
nameWithNamespace: 'Administrator / Vulnerability reports', nameWithNamespace: 'Administrator / Vulnerability reports',
......
...@@ -46,6 +46,7 @@ describe('Vulnerability list component', () => { ...@@ -46,6 +46,7 @@ describe('Vulnerability list component', () => {
); );
}; };
const locationText = ({ file, startLine }) => `${file}:${startLine}`;
const findTable = () => wrapper.findComponent(GlTable); const findTable = () => wrapper.findComponent(GlTable);
const findSortableColumn = () => wrapper.find('[aria-sort="descending"]'); const findSortableColumn = () => wrapper.find('[aria-sort="descending"]');
const findCell = (label) => wrapper.find(`.js-${label}`); const findCell = (label) => wrapper.find(`.js-${label}`);
...@@ -62,6 +63,7 @@ describe('Vulnerability list component', () => { ...@@ -62,6 +63,7 @@ describe('Vulnerability list component', () => {
findRow(row).findComponent(VulnerabilityCommentIcon); findRow(row).findComponent(VulnerabilityCommentIcon);
const findDataCell = (label) => wrapper.findByTestId(label); const findDataCell = (label) => wrapper.findByTestId(label);
const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`); const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`);
const findLocationTextWrapper = (cell) => cell.find(GlTruncate); const findLocationTextWrapper = (cell) => cell.find(GlTruncate);
const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults); const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
const findDashboardHasNoVulnerabilities = () => const findDashboardHasNoVulnerabilities = () =>
...@@ -232,7 +234,7 @@ describe('Vulnerability list component', () => { ...@@ -232,7 +234,7 @@ describe('Vulnerability list component', () => {
it('should display the vulnerability locations for images', () => { it('should display the vulnerability locations for images', () => {
const { id, project, location } = newVulnerabilities[0]; const { id, project, location } = newVulnerabilities[0];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).toContain(project.nameWithNamespace); expect(cell.text()).toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: location.image, text: location.image,
...@@ -242,17 +244,17 @@ describe('Vulnerability list component', () => { ...@@ -242,17 +244,17 @@ describe('Vulnerability list component', () => {
it('should display the vulnerability locations for code', () => { it('should display the vulnerability locations for code', () => {
const { id, project, location } = newVulnerabilities[1]; const { id, project, location } = newVulnerabilities[1];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).toContain(project.nameWithNamespace); expect(cell.text()).toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: `${location.file} (line: ${location.startLine})`, text: locationText(location),
position: 'middle', position: 'middle',
}); });
}); });
it('should display the vulnerability locations for code with no line data', () => { it('should display the vulnerability locations for code with no line data', () => {
const { id, project, location } = newVulnerabilities[2]; const { id, project, location } = newVulnerabilities[2];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).toContain(project.nameWithNamespace); expect(cell.text()).toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: location.file, text: location.file,
...@@ -262,14 +264,14 @@ describe('Vulnerability list component', () => { ...@@ -262,14 +264,14 @@ describe('Vulnerability list component', () => {
it('should not display the vulnerability locations for vulnerabilities without a location', () => { it('should not display the vulnerability locations for vulnerabilities without a location', () => {
const { id, project } = newVulnerabilities[4]; const { id, project } = newVulnerabilities[4];
const cellText = findDataCell(`location-${id}`).text(); const cellText = findLocationCell(id).text();
expect(cellText).toEqual(project.nameWithNamespace); expect(cellText).toEqual(project.nameWithNamespace);
expect(cellText).not.toContain('(line: '); expect(cellText).not.toContain(':');
}); });
it('should display the vulnerability locations for path', () => { it('should display the vulnerability locations for path', () => {
const { id, project, location } = newVulnerabilities[5]; const { id, project, location } = newVulnerabilities[5];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).toContain(project.nameWithNamespace); expect(cell.text()).toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: location.path, text: location.path,
...@@ -293,7 +295,7 @@ describe('Vulnerability list component', () => { ...@@ -293,7 +295,7 @@ describe('Vulnerability list component', () => {
it('should not display the vulnerability group/project locations for images', () => { it('should not display the vulnerability group/project locations for images', () => {
const { id, project, location } = newVulnerabilities[0]; const { id, project, location } = newVulnerabilities[0];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).not.toContain(project.nameWithNamespace); expect(cell.text()).not.toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: location.image, text: location.image,
...@@ -310,17 +312,29 @@ describe('Vulnerability list component', () => { ...@@ -310,17 +312,29 @@ describe('Vulnerability list component', () => {
it('should display the vulnerability locations for code', () => { it('should display the vulnerability locations for code', () => {
const { id, project, location } = newVulnerabilities[1]; const { id, project, location } = newVulnerabilities[1];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).not.toContain(project.nameWithNamespace); expect(cell.text()).not.toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: `${location.file} (line: ${location.startLine})`, text: locationText(location),
position: 'middle', position: 'middle',
}); });
}); });
it('should make the file path linkable', () => {
const { id, location } = newVulnerabilities[1];
const cell = findLocationCell(id);
expect(cell.find('a').attributes('href')).toBe(`${location.blobPath}#L${location.startLine}`);
});
it('should not make the file path linkable if blobPath is missing', () => {
const { id } = newVulnerabilities[0];
const cell = findLocationCell(id);
expect(cell.find('a').exists()).toBe(false);
});
it('should not display the vulnerability group/project locations for code with no line data', () => { it('should not display the vulnerability group/project locations for code with no line data', () => {
const { id, project, location } = newVulnerabilities[2]; const { id, project, location } = newVulnerabilities[2];
const cell = findDataCell(`location-${id}`); const cell = findLocationCell(id);
expect(cell.text()).not.toContain(project.nameWithNamespace); expect(cell.text()).not.toContain(project.nameWithNamespace);
expect(findLocationTextWrapper(cell).props()).toEqual({ expect(findLocationTextWrapper(cell).props()).toEqual({
text: location.file, text: location.file,
......
...@@ -146,7 +146,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do ...@@ -146,7 +146,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
end end
it 'returns false when running inside a Rake task' do it 'returns false when running inside a Rake task' do
expect(described_class).to receive(:program_name).and_return('rake') allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
expect(described_class.enable?).to eq(false) expect(described_class.enable?).to eq(false)
end end
......
# frozen_string_literal: true
RSpec.shared_examples_for 'subscription form data' do |js_selector|
before do
allow(view).to receive(:subscription_data).and_return(
setup_for_company: 'true',
full_name: 'First Last',
plan_data: '[{"id":"bronze_id","code":"bronze","price_per_year":48.0}]',
plan_id: 'bronze_id'
)
end
subject { render }
it { is_expected.to have_selector("#{js_selector}[data-setup-for-company='true']") }
it { is_expected.to have_selector("#{js_selector}[data-full-name='First Last']") }
it { is_expected.to have_selector("#{js_selector}[data-plan-data='[{\"id\":\"bronze_id\",\"code\":\"bronze\",\"price_per_year\":48.0}]']") }
it { is_expected.to have_selector("#{js_selector}[data-plan-id='bronze_id']") }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'subscriptions/buy_minutes' do
it_behaves_like 'subscription form data', '#js-buy-minutes'
end
...@@ -3,19 +3,5 @@ ...@@ -3,19 +3,5 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'subscriptions/new' do RSpec.describe 'subscriptions/new' do
before do it_behaves_like 'subscription form data', '#js-new-subscription'
allow(view).to receive(:subscription_data).and_return(
setup_for_company: 'true',
full_name: 'First Last',
plan_data: '[{"id":"bronze_id","code":"bronze","price_per_year":48.0}]',
plan_id: 'bronze_id'
)
end
subject { render }
it { is_expected.to have_selector("#checkout[data-setup-for-company='true']") }
it { is_expected.to have_selector("#checkout[data-full-name='First Last']") }
it { is_expected.to have_selector("#checkout[data-plan-data='[{\"id\":\"bronze_id\",\"code\":\"bronze\",\"price_per_year\":48.0}]']") }
it { is_expected.to have_selector("#checkout[data-plan-id='bronze_id']") }
end end
...@@ -199,10 +199,7 @@ module API ...@@ -199,10 +199,7 @@ module API
user_project = find_project_with_access(params) user_project = find_project_with_access(params)
merge_requests = authorized_merge_requests_for_project(user_project) merge_requests = authorized_merge_requests_for_project(user_project)
merge_requests = merge_requests.preload(:author, :assignees, :metrics, source_project: :namespace, target_project: :namespace)
if Feature.enabled?(:api_v3_repos_events_optimization, user_project)
merge_requests = merge_requests.preload(:author, :assignees, :metrics, source_project: :namespace, target_project: :namespace)
end
present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
end end
......
...@@ -1031,9 +1031,6 @@ msgstr "" ...@@ -1031,9 +1031,6 @@ msgstr ""
msgid "(deleted)" msgid "(deleted)"
msgstr "" msgstr ""
msgid "(line: %{startLine})"
msgstr ""
msgid "(max size 15 MB)" msgid "(max size 15 MB)"
msgstr "" msgstr ""
...@@ -5202,6 +5199,9 @@ msgstr "" ...@@ -5202,6 +5199,9 @@ msgstr ""
msgid "Busy" msgid "Busy"
msgstr "" msgstr ""
msgid "Buy CI Minutes"
msgstr ""
msgid "Buy License" msgid "Buy License"
msgstr "" msgstr ""
...@@ -16546,9 +16546,6 @@ msgstr "" ...@@ -16546,9 +16546,6 @@ msgstr ""
msgid "Invite members" msgid "Invite members"
msgstr "" msgstr ""
msgid "Invite team members"
msgstr ""
msgid "Invite your team" msgid "Invite your team"
msgstr "" msgstr ""
...@@ -22031,9 +22028,6 @@ msgstr "" ...@@ -22031,9 +22028,6 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "Pipeline cannot be run."
msgstr ""
msgid "Pipeline minutes quota" msgid "Pipeline minutes quota"
msgstr "" msgstr ""
...@@ -22292,6 +22286,9 @@ msgstr "" ...@@ -22292,6 +22286,9 @@ msgstr ""
msgid "Pipeline|Branch name" msgid "Pipeline|Branch name"
msgstr "" msgstr ""
msgid "Pipeline|Branches or tags could not be loaded."
msgstr ""
msgid "Pipeline|Canceled" msgid "Pipeline|Canceled"
msgstr "" msgstr ""
...@@ -22352,6 +22349,9 @@ msgstr "" ...@@ -22352,6 +22349,9 @@ msgstr ""
msgid "Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}" msgid "Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}"
msgstr "" msgstr ""
msgid "Pipeline|Pipeline cannot be run."
msgstr ""
msgid "Pipeline|Pipelines" msgid "Pipeline|Pipelines"
msgstr "" msgstr ""
......
...@@ -474,16 +474,4 @@ describe('issue_comment_form component', () => { ...@@ -474,16 +474,4 @@ describe('issue_comment_form component', () => {
expect(findTextArea().exists()).toBe(false); expect(findTextArea().exists()).toBe(false);
}); });
}); });
describe('close/reopen button variants', () => {
it.each([
[constants.OPENED, 'warning'],
[constants.REOPENED, 'warning'],
[constants.CLOSED, 'default'],
])('when %s, the variant of the btn is %s', (state, expected) => {
mountComponent({ noteableData: { ...noteableDataMock, state } });
expect(findCloseReopenButton().props('variant')).toBe(expected);
});
});
}); });
import { GlDropdown, GlDropdownItem, GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -6,34 +6,26 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,34 +6,26 @@ import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import { import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
mockBranches, import { mockQueryParams, mockPostParams, mockProjectId, mockError, mockRefs } from '../mock_data';
mockTags,
mockParams,
mockPostParams,
mockProjectId,
mockError,
} from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(), redirectTo: jest.fn(),
})); }));
const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines'; const pipelinesPath = '/root/project/-/pipelines';
const configVariablesPath = '/root/project/-/pipelines/config_variables'; const configVariablesPath = '/root/project/-/pipelines/config_variables';
const postResponse = { id: 1 }; const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'master';
describe('Pipeline New Form', () => { describe('Pipeline New Form', () => {
let wrapper; let wrapper;
let mock; let mock;
let dummySubmitEvent;
const dummySubmitEvent = {
preventDefault() {},
};
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findDropdown = () => wrapper.find(GlDropdown); const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
...@@ -44,33 +36,42 @@ describe('Pipeline New Form', () => { ...@@ -44,33 +36,42 @@ describe('Pipeline New Form', () => {
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf); const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data); const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
const changeRef = (i) => findDropdownItems().at(i).vm.$emit('click');
const selectBranch = (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
shortName: branch,
fullName: `refs/heads/${branch}`,
});
};
const createComponent = (term = '', props = {}, method = shallowMount) => { const createComponent = (props = {}, method = shallowMount) => {
wrapper = method(PipelineNewForm, { wrapper = method(PipelineNewForm, {
provide: {
projectRefsEndpoint,
},
propsData: { propsData: {
projectId: mockProjectId, projectId: mockProjectId,
pipelinesPath, pipelinesPath,
configVariablesPath, configVariablesPath,
branches: mockBranches, defaultBranch,
tags: mockTags, refParam: defaultBranch,
defaultBranch: 'master',
settingsLink: '', settingsLink: '',
maxWarnings: 25, maxWarnings: 25,
...props, ...props,
}, },
data() {
return {
searchTerm: term,
};
},
}); });
}; };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
dummySubmitEvent = {
preventDefault: jest.fn(),
};
}); });
afterEach(() => { afterEach(() => {
...@@ -80,38 +81,17 @@ describe('Pipeline New Form', () => { ...@@ -80,38 +81,17 @@ describe('Pipeline New Form', () => {
mock.restore(); mock.restore();
}); });
describe('Dropdown with branches and tags', () => {
beforeEach(() => {
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse);
});
it('displays dropdown with all branches and tags', () => {
const refLength = mockBranches.length + mockTags.length;
createComponent();
expect(findDropdownItems()).toHaveLength(refLength);
});
it('when user enters search term the list is filtered', () => {
createComponent('master');
expect(findDropdownItems()).toHaveLength(1);
expect(findDropdownItems().at(0).text()).toBe('master');
});
});
describe('Form', () => { describe('Form', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent('', mockParams, mount); createComponent(mockQueryParams, mount);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
await waitForPromises(); await waitForPromises();
}); });
it('displays the correct values for the provided query params', async () => { it('displays the correct values for the provided query params', async () => {
expect(findDropdown().props('text')).toBe('tag-1'); expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
expect(findVariableRows()).toHaveLength(3); expect(findVariableRows()).toHaveLength(3);
}); });
...@@ -152,11 +132,19 @@ describe('Pipeline New Form', () => { ...@@ -152,11 +132,19 @@ describe('Pipeline New Form', () => {
describe('Pipeline creation', () => { describe('Pipeline creation', () => {
beforeEach(async () => { beforeEach(async () => {
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
await waitForPromises(); await waitForPromises();
}); });
it('does not submit the native HTML form', async () => {
createComponent();
findForm().vm.$emit('submit', dummySubmitEvent);
expect(dummySubmitEvent.preventDefault).toHaveBeenCalled();
});
it('disables the submit button immediately after submitting', async () => { it('disables the submit button immediately after submitting', async () => {
createComponent(); createComponent();
...@@ -171,19 +159,15 @@ describe('Pipeline New Form', () => { ...@@ -171,19 +159,15 @@ describe('Pipeline New Form', () => {
it('creates pipeline with full ref and variables', async () => { it('creates pipeline with full ref and variables', async () => {
createComponent(); createComponent();
changeRef(0);
findForm().vm.$emit('submit', dummySubmitEvent); findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises(); await waitForPromises();
expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName); expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
}); });
it('creates a pipeline with short ref and variables', async () => { it('creates a pipeline with short ref and variables from the query params', async () => {
// query params are used createComponent(mockQueryParams);
createComponent('', mockParams);
await waitForPromises(); await waitForPromises();
...@@ -191,19 +175,19 @@ describe('Pipeline New Form', () => { ...@@ -191,19 +175,19 @@ describe('Pipeline New Form', () => {
await waitForPromises(); await waitForPromises();
expect(getExpectedPostParams()).toEqual(mockPostParams); expect(getFormPostParams()).toEqual(mockPostParams);
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
}); });
}); });
describe('When the ref has been changed', () => { describe('When the ref has been changed', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent('', {}, mount); createComponent({}, mount);
await waitForPromises(); await waitForPromises();
}); });
it('variables persist between ref changes', async () => { it('variables persist between ref changes', async () => {
changeRef(0); // change to master selectBranch('master');
await waitForPromises(); await waitForPromises();
...@@ -213,7 +197,7 @@ describe('Pipeline New Form', () => { ...@@ -213,7 +197,7 @@ describe('Pipeline New Form', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
changeRef(1); // change to branch-1 selectBranch('branch-1');
await waitForPromises(); await waitForPromises();
...@@ -223,14 +207,14 @@ describe('Pipeline New Form', () => { ...@@ -223,14 +207,14 @@ describe('Pipeline New Form', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
changeRef(0); // change back to master selectBranch('master');
await waitForPromises(); await waitForPromises();
expect(findKeyInputs().at(0).element.value).toBe('build_var'); expect(findKeyInputs().at(0).element.value).toBe('build_var');
expect(findVariableRows().length).toBe(2); expect(findVariableRows().length).toBe(2);
changeRef(1); // change back to branch-1 selectBranch('branch-1');
await waitForPromises(); await waitForPromises();
...@@ -248,7 +232,7 @@ describe('Pipeline New Form', () => { ...@@ -248,7 +232,7 @@ describe('Pipeline New Form', () => {
const mockYmlDesc = 'A var from yml.'; const mockYmlDesc = 'A var from yml.';
it('loading icon is shown when content is requested and hidden when received', async () => { it('loading icon is shown when content is requested and hidden when received', async () => {
createComponent('', mockParams, mount); createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: { [mockYmlKey]: {
...@@ -265,7 +249,7 @@ describe('Pipeline New Form', () => { ...@@ -265,7 +249,7 @@ describe('Pipeline New Form', () => {
}); });
it('multi-line strings are added to the value field without removing line breaks', async () => { it('multi-line strings are added to the value field without removing line breaks', async () => {
createComponent('', mockParams, mount); createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: { [mockYmlKey]: {
...@@ -281,7 +265,7 @@ describe('Pipeline New Form', () => { ...@@ -281,7 +265,7 @@ describe('Pipeline New Form', () => {
describe('with description', () => { describe('with description', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent('', mockParams, mount); createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: { [mockYmlKey]: {
...@@ -323,7 +307,7 @@ describe('Pipeline New Form', () => { ...@@ -323,7 +307,7 @@ describe('Pipeline New Form', () => {
describe('without description', () => { describe('without description', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent('', mockParams, mount); createComponent(mockQueryParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: { [mockYmlKey]: {
...@@ -346,6 +330,21 @@ describe('Pipeline New Form', () => { ...@@ -346,6 +330,21 @@ describe('Pipeline New Form', () => {
createComponent(); createComponent();
}); });
describe('when the refs cannot be loaded', () => {
beforeEach(() => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
findRefsDropdown().vm.$emit('loadingError');
});
it('shows both an error alert', () => {
expect(findErrorAlert().exists()).toBe(true);
expect(findWarningAlert().exists()).toBe(false);
});
});
describe('when the error response can be handled', () => { describe('when the error response can be handled', () => {
beforeEach(async () => { beforeEach(async () => {
mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
......
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
import { mockRefs, mockFilteredRefs } from '../mock_data';
const projectRefsEndpoint = '/root/project/refs';
const refShortName = 'master';
const refFullName = 'refs/heads/master';
jest.mock('~/flash');
describe('Pipeline New Form', () => {
let wrapper;
let mock;
const findDropdown = () => wrapper.find(GlDropdown);
const findRefsDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(RefsDropdown, {
provide: {
projectRefsEndpoint,
},
propsData: {
value: {
shortName: refShortName,
fullName: refFullName,
},
...props,
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(httpStatusCodes.OK, mockRefs);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
beforeEach(() => {
createComponent();
});
it('displays empty dropdown initially', async () => {
await findDropdown().vm.$emit('show');
expect(findRefsDropdownItems()).toHaveLength(0);
});
it('does not make requests immediately', async () => {
expect(mock.history.get).toHaveLength(0);
});
describe('when user opens dropdown', () => {
beforeEach(async () => {
await findDropdown().vm.$emit('show');
await waitForPromises();
});
it('requests unfiltered tags and branches', async () => {
expect(mock.history.get).toHaveLength(1);
expect(mock.history.get[0].url).toBe(projectRefsEndpoint);
expect(mock.history.get[0].params).toEqual({ search: '' });
});
it('displays dropdown with branches and tags', async () => {
const refLength = mockRefs.Tags.length + mockRefs.Branches.length;
expect(findRefsDropdownItems()).toHaveLength(refLength);
});
it('displays the names of refs', () => {
// Branches
expect(findRefsDropdownItems().at(0).text()).toBe(mockRefs.Branches[0]);
// Tags (appear after branches)
const firstTag = mockRefs.Branches.length;
expect(findRefsDropdownItems().at(firstTag).text()).toBe(mockRefs.Tags[0]);
});
it('when user shows dropdown a second time, only one request is done', () => {
expect(mock.history.get).toHaveLength(1);
});
describe('when user selects a value', () => {
const selectedIndex = 1;
beforeEach(async () => {
await findRefsDropdownItems().at(selectedIndex).vm.$emit('click');
});
it('component emits @input', () => {
const inputs = wrapper.emitted('input');
expect(inputs).toHaveLength(1);
expect(inputs[0]).toEqual([{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' }]);
});
});
describe('when user types searches for a tag', () => {
const mockSearchTerm = 'my-search';
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, { params: { search: mockSearchTerm } })
.reply(httpStatusCodes.OK, mockFilteredRefs);
await findSearchBox().vm.$emit('input', mockSearchTerm);
await waitForPromises();
});
it('requests filtered tags and branches', async () => {
expect(mock.history.get).toHaveLength(2);
expect(mock.history.get[1].params).toEqual({
search: mockSearchTerm,
});
});
it('displays dropdown with branches and tags', async () => {
const filteredRefLength = mockFilteredRefs.Tags.length + mockFilteredRefs.Branches.length;
expect(findRefsDropdownItems()).toHaveLength(filteredRefLength);
});
});
});
describe('when user has selected a value', () => {
const selectedIndex = 1;
const mockShortName = mockRefs.Branches[selectedIndex];
const mockFullName = `refs/heads/${mockShortName}`;
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, {
params: { ref: mockFullName },
})
.reply(httpStatusCodes.OK, mockRefs);
createComponent({
value: {
shortName: mockShortName,
fullName: mockFullName,
},
});
await findDropdown().vm.$emit('show');
await waitForPromises();
});
it('branch is checked', () => {
expect(findRefsDropdownItems().at(selectedIndex).props('isChecked')).toBe(true);
});
});
describe('when server returns an error', () => {
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
await findDropdown().vm.$emit('show');
await waitForPromises();
});
it('loading error event is emitted', () => {
expect(wrapper.emitted('loadingError')).toHaveLength(1);
expect(wrapper.emitted('loadingError')[0]).toEqual([expect.any(Error)]);
});
});
});
export const mockBranches = [ export const mockRefs = {
{ shortName: 'master', fullName: 'refs/heads/master' }, Branches: ['master', 'branch-1', 'branch-2'],
{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' }, Tags: ['1.0.0', '1.1.0', '1.2.0'],
{ shortName: 'branch-2', fullName: 'refs/heads/branch-2' }, };
];
export const mockTags = [ export const mockFilteredRefs = {
{ shortName: '1.0.0', fullName: 'refs/tags/1.0.0' }, Branches: ['branch-1'],
{ shortName: '1.1.0', fullName: 'refs/tags/1.1.0' }, Tags: ['1.0.0', '1.1.0'],
{ shortName: '1.2.0', fullName: 'refs/tags/1.2.0' }, };
];
export const mockParams = { export const mockQueryParams = {
refParam: 'tag-1', refParam: 'tag-1',
variableParams: { variableParams: {
test_var: 'test_var_val', test_var: 'test_var_val',
......
...@@ -187,23 +187,6 @@ RSpec.describe API::V3::Github do ...@@ -187,23 +187,6 @@ RSpec.describe API::V3::Github do
expect { jira_get v3_api(events_path, user) }.not_to exceed_all_query_limit(control_count) expect { jira_get v3_api(events_path, user) }.not_to exceed_all_query_limit(control_count)
end end
context 'with `api_v3_repos_events_optimization` feature flag disabled' do
before do
stub_feature_flags(api_v3_repos_events_optimization: false)
end
it 'falls back to less optimal query performance' do
create(:merge_request, source_project: project)
source_project = fork_project(project, nil, repository: true)
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { jira_get v3_api(events_path, user) }.count
create_list(:merge_request, 2, :unique_branches, source_project: source_project, target_project: project)
expect { jira_get v3_api(events_path, user) }.to exceed_all_query_limit(control_count)
end
end
context 'if there are more merge requests' do context 'if there are more merge requests' do
let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) } let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) }
let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) } let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/show.html.haml' do
let_it_be(:user) { build(:user) }
let_it_be(:group) { create(:group) }
before do
assign(:group, group)
end
context 'when rendering with the layout' do
subject(:render_page) { render template: 'groups/show.html.haml', layout: 'layouts/group' }
describe 'invite team members' do
before do
allow(view).to receive(:session).and_return({})
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:experiment_enabled?).and_return(false)
allow(view).to receive(:group_path).and_return('')
allow(view).to receive(:group_shared_path).and_return('')
allow(view).to receive(:group_archived_path).and_return('')
end
context 'when invite team members is not available in sidebar' do
before do
allow(view).to receive(:can_invite_members_for_group?).and_return(false)
end
it 'does not display the js-invite-members-trigger' do
render_page
expect(rendered).not_to have_selector('.js-invite-members-trigger')
end
end
context 'when invite team members is available' do
before do
allow(view).to receive(:can_invite_members_for_group?).and_return(true)
end
it 'includes the div for js-invite-members-trigger' do
render_page
expect(rendered).to have_selector('.js-invite-members-trigger')
end
end
end
end
end
...@@ -79,41 +79,4 @@ RSpec.describe 'projects/empty' do ...@@ -79,41 +79,4 @@ RSpec.describe 'projects/empty' do
it_behaves_like 'no invite member info' it_behaves_like 'no invite member info'
end end
end end
context 'when rendering with the layout' do
subject(:render_page) { render template: 'projects/empty.html.haml', layout: 'layouts/project' }
describe 'invite team members' do
before do
allow(view).to receive(:session).and_return({})
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:experiment_enabled?).and_return(false)
end
context 'when invite team members is not available in sidebar' do
before do
allow(view).to receive(:can_invite_members_for_project?).and_return(false)
end
it 'does not display the js-invite-members-trigger' do
render_page
expect(rendered).not_to have_selector('.js-invite-members-trigger')
end
end
context 'when invite team members is available' do
before do
allow(view).to receive(:can_invite_members_for_project?).and_return(true)
end
it 'includes the div for js-invite-members-trigger' do
render_page
expect(rendered).to have_selector('.js-invite-members-trigger')
end
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/show.html.haml' do
let_it_be(:user) { build(:user) }
let_it_be(:project) { ProjectPresenter.new(create(:project, :repository), current_user: user) }
before do
assign(:project, project)
end
context 'when rendering with the layout' do
subject(:render_page) { render template: 'projects/show.html.haml', layout: 'layouts/project' }
describe 'invite team members' do
before do
allow(view).to receive(:event_filter_link)
allow(view).to receive(:session).and_return({})
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:experiment_enabled?).and_return(false)
allow(view).to receive(:add_page_startup_graphql_call)
end
context 'when invite team members is not available in sidebar' do
before do
allow(view).to receive(:can_invite_members_for_project?).and_return(false)
end
it 'does not display the js-invite-members-trigger' do
render_page
expect(rendered).not_to have_selector('.js-invite-members-trigger')
end
end
context 'when invite team members is available' do
before do
allow(view).to receive(:can_invite_members_for_project?).and_return(true)
end
it 'includes the div for js-invite-members-trigger' do
render_page
expect(rendered).to have_selector('.js-invite-members-trigger')
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment