Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
92fa4c9f53
commit
07959a9d0d
|
|
@ -34,7 +34,6 @@ Capybara/VisibilityMatcher:
|
|||
- 'spec/features/merge_request/user_views_diffs_commit_spec.rb'
|
||||
- 'spec/features/merge_request/user_views_diffs_spec.rb'
|
||||
- 'spec/features/projects/blobs/blob_show_spec.rb'
|
||||
- 'spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb'
|
||||
- 'spec/features/projects/ci/lint_spec.rb'
|
||||
- 'spec/features/projects/commit/comments/user_adds_comment_spec.rb'
|
||||
- 'spec/features/projects/commits/multi_view_diff_spec.rb'
|
||||
|
|
|
|||
|
|
@ -3117,7 +3117,6 @@ Layout/LineLength:
|
|||
- 'spec/features/projects/blobs/blob_show_spec.rb'
|
||||
- 'spec/features/projects/blobs/edit_spec.rb'
|
||||
- 'spec/features/projects/blobs/shortcuts_blob_spec.rb'
|
||||
- 'spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb'
|
||||
- 'spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb'
|
||||
- 'spec/features/projects/ci/editor_spec.rb'
|
||||
- 'spec/features/projects/commit/cherry_pick_spec.rb'
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
suggestCiYmlData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -56,7 +51,6 @@ export default {
|
|||
:filename="filename"
|
||||
:templates="templates"
|
||||
:initial-template="initialTemplate"
|
||||
:suggest-ci-yml-data="suggestCiYmlData"
|
||||
@selected="onTemplateSelected"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import SuggestGitlabCiYml from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
|
||||
|
||||
|
|
@ -34,7 +33,6 @@ const templateSelectors = [
|
|||
export default {
|
||||
name: 'TemplateSelector',
|
||||
components: {
|
||||
SuggestGitlabCiYml,
|
||||
GlCollapsibleListbox,
|
||||
},
|
||||
props: {
|
||||
|
|
@ -51,11 +49,6 @@ export default {
|
|||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
suggestCiYmlData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -97,9 +90,6 @@ export default {
|
|||
showDropdown() {
|
||||
return this.activeType && this.templateItems.length > 0;
|
||||
},
|
||||
showPopover() {
|
||||
return this.activeType?.key === 'gitlab_ci_ymls' && this.suggestCiYmlData;
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.activeType) this.applyTemplate(this.initialTemplate);
|
||||
|
|
@ -135,14 +125,6 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div v-if="showDropdown">
|
||||
<suggest-gitlab-ci-yml
|
||||
v-if="showPopover"
|
||||
target="template-selector"
|
||||
:track-label="suggestCiYmlData.trackLabel"
|
||||
:dismiss-key="suggestCiYmlData.dismissKey"
|
||||
:merge-request-path="suggestCiYmlData.mergeRequestPath"
|
||||
:human-access="suggestCiYmlData.humanAccess"
|
||||
/>
|
||||
<gl-collapsible-listbox
|
||||
id="template-selector"
|
||||
searchable
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import FilepathForm from './components/filepath_form.vue';
|
||||
|
||||
const getPopoverData = (el) => ({
|
||||
trackLabel: el.dataset.trackLabel,
|
||||
dismissKey: el.dataset.dismissKey,
|
||||
mergeRequestPath: el.dataset.mergeRequestPath,
|
||||
humanAccess: el.dataset.humanAccess,
|
||||
});
|
||||
|
||||
const getInputOptions = (el) => {
|
||||
const { testid, qa_selector: qaSelector, ...options } = JSON.parse(el.dataset.inputOptions);
|
||||
return {
|
||||
|
|
@ -19,15 +12,11 @@ const getInputOptions = (el) => {
|
|||
export default ({ onTemplateSelected }) => {
|
||||
const el = document.getElementById('js-template-selectors-menu');
|
||||
|
||||
const suggestCiYmlEl = document.querySelector('.js-suggest-gitlab-ci-yml');
|
||||
const suggestCiYmlData = suggestCiYmlEl ? getPopoverData(suggestCiYmlEl) : undefined;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(h) {
|
||||
return h(FilepathForm, {
|
||||
props: {
|
||||
suggestCiYmlData,
|
||||
inputOptions: getInputOptions(el),
|
||||
templates: JSON.parse(el.dataset.templates),
|
||||
initialTemplate: el.dataset.selected,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
import Api from '~/api';
|
||||
import initPopover from '~/blob/suggest_gitlab_ci_yml';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import toast from '~/vue_shared/plugins/global_toast';
|
||||
|
|
@ -33,7 +32,6 @@ export default class FilepathFormMediator {
|
|||
|
||||
selectTemplateFile(template, type, clearSelectedTemplate, stopLoading) {
|
||||
const self = this;
|
||||
const suggestCommitChanges = document.querySelector('.js-suggest-gitlab-ci-yml-commit-changes');
|
||||
|
||||
this.fetchFileTemplate(type.type, template.key, template)
|
||||
.then((file) => {
|
||||
|
|
@ -50,10 +48,6 @@ export default class FilepathFormMediator {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (suggestCommitChanges) {
|
||||
initPopover(suggestCommitChanges);
|
||||
}
|
||||
})
|
||||
.catch((err) =>
|
||||
createAlert({
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
<script>
|
||||
import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
|
||||
import { getCookie, removeCookie } from '~/lib/utils/common_utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
const trackingMixin = Tracking.mixin();
|
||||
|
||||
export default {
|
||||
beginnerLink:
|
||||
'https://about.gitlab.com/blog/2018/01/22/a-beginners-guide-to-continuous-integration/',
|
||||
goToTrackValuePipelines: 10,
|
||||
goToTrackValueMergeRequest: 20,
|
||||
trackEvent: 'click_button',
|
||||
components: {
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
GlLink,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
props: {
|
||||
goToPipelinesPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectMergeRequestsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
commitCookie: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
humanAccess: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
exampleLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
codeQualityLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
trackLabel: 'congratulate_first_pipeline',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tracking() {
|
||||
return {
|
||||
label: this.trackLabel,
|
||||
property: this.humanAccess,
|
||||
};
|
||||
},
|
||||
goToMergeRequestPath() {
|
||||
return this.commitCookiePath || this.projectMergeRequestsPath;
|
||||
},
|
||||
commitCookiePath() {
|
||||
const cookieVal = getCookie(this.commitCookie);
|
||||
|
||||
if (cookieVal !== 'true') return cookieVal;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
modalTitle: __("That's it, well done!"),
|
||||
pipelinesButton: s__('MR widget|See your pipeline in action'),
|
||||
mergeRequestButton: s__('MR widget|Back to the merge request'),
|
||||
bodyMessage: s__(
|
||||
`MR widget|The pipeline will test your code on every commit. A %{codeQualityLinkStart}code quality report%{codeQualityLinkEnd} will appear in your merge requests to warn you about potential code degradations.`,
|
||||
),
|
||||
helpMessage: s__(
|
||||
`MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more.`,
|
||||
),
|
||||
},
|
||||
mounted() {
|
||||
this.track();
|
||||
this.disableModalFromRenderingAgain();
|
||||
},
|
||||
methods: {
|
||||
disableModalFromRenderingAgain() {
|
||||
removeCookie(this.commitCookie);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal visible size="sm" modal-id="success-pipeline-modal-id-not-used">
|
||||
<template #modal-title>
|
||||
{{ $options.i18n.modalTitle }}
|
||||
<gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" data-name="tada" />
|
||||
</template>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.bodyMessage">
|
||||
<template #codeQualityLink="{ content }">
|
||||
<gl-link :href="codeQualityLink" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<gl-sprintf :message="$options.i18n.helpMessage">
|
||||
<template #beginnerLink="{ content }">
|
||||
<gl-link :href="$options.beginnerLink" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
<template #exampleLink="{ content }">
|
||||
<gl-link :href="exampleLink" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<template #modal-footer>
|
||||
<gl-button
|
||||
v-if="projectMergeRequestsPath"
|
||||
ref="goToMergeRequest"
|
||||
:href="goToMergeRequestPath"
|
||||
:data-track-property="humanAccess"
|
||||
:data-track-value="$options.goToTrackValueMergeRequest"
|
||||
:data-track-action="$options.trackEvent"
|
||||
:data-track-label="trackLabel"
|
||||
>
|
||||
{{ $options.i18n.mergeRequestButton }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="goToPipelines"
|
||||
:href="goToPipelinesPath"
|
||||
variant="confirm"
|
||||
:data-track-property="humanAccess"
|
||||
:data-track-value="$options.goToTrackValuePipelines"
|
||||
:data-track-action="$options.trackEvent"
|
||||
:data-track-label="trackLabel"
|
||||
>
|
||||
{{ $options.i18n.pipelinesButton }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
<script>
|
||||
import { GlPopover, GlSprintf, GlButton } from '@gitlab/ui';
|
||||
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
const trackingMixin = Tracking.mixin();
|
||||
|
||||
const popoverStates = {
|
||||
suggest_gitlab_ci_yml: {
|
||||
title: s__(`suggestPipeline|1/2: Choose a template`),
|
||||
content: s__(
|
||||
`suggestPipeline|We’re adding a GitLab CI configuration file to add a pipeline to the project. You could create it manually, but we recommend that you start with a GitLab template that works out of the box.`,
|
||||
),
|
||||
footer: s__(
|
||||
`suggestPipeline|Choose %{boldStart}Code Quality%{boldEnd} to add a pipeline that tests the quality of your code.`,
|
||||
),
|
||||
},
|
||||
suggest_commit_first_project_gitlab_ci_yml: {
|
||||
title: s__(`suggestPipeline|2/2: Commit your changes`),
|
||||
content: s__(
|
||||
`suggestPipeline|The template is ready! You can now commit it to create your first pipeline.`,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'SuggestGitlabCiYml',
|
||||
dismissTrackValue: 10,
|
||||
clickTrackValue: 'click_button',
|
||||
components: {
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
props: {
|
||||
target: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
trackLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dismissKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
humanAccess: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mergeRequestPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popoverDismissed: parseBoolean(getCookie(`${this.trackLabel}_${this.dismissKey}`)),
|
||||
tracking: {
|
||||
label: this.trackLabel,
|
||||
property: this.humanAccess,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
suggestTitle() {
|
||||
return popoverStates[this.trackLabel].title || '';
|
||||
},
|
||||
suggestContent() {
|
||||
return popoverStates[this.trackLabel].content || '';
|
||||
},
|
||||
suggestFooter() {
|
||||
return popoverStates[this.trackLabel].footer || '';
|
||||
},
|
||||
emoji() {
|
||||
return popoverStates[this.trackLabel].emoji || '';
|
||||
},
|
||||
dismissCookieName() {
|
||||
return `${this.trackLabel}_${this.dismissKey}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (
|
||||
this.trackLabel === 'suggest_commit_first_project_gitlab_ci_yml' &&
|
||||
!this.popoverDismissed
|
||||
) {
|
||||
scrollToElement(document.querySelector(this.target));
|
||||
}
|
||||
|
||||
this.trackOnShow();
|
||||
},
|
||||
methods: {
|
||||
onDismiss() {
|
||||
this.popoverDismissed = true;
|
||||
setCookie(this.dismissCookieName, this.popoverDismissed);
|
||||
},
|
||||
trackOnShow() {
|
||||
if (!this.popoverDismissed) this.track();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-popover
|
||||
v-if="!popoverDismissed"
|
||||
show
|
||||
:target="target"
|
||||
placement="right"
|
||||
container="viewport"
|
||||
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ suggestTitle }}</span>
|
||||
<span class="ml-auto">
|
||||
<gl-button
|
||||
:aria-label="__('Close')"
|
||||
class="btn-blank"
|
||||
name="dismiss"
|
||||
icon="close"
|
||||
:data-track-property="humanAccess"
|
||||
:data-track-value="$options.dismissTrackValue"
|
||||
:data-track-action="$options.clickTrackValue"
|
||||
:data-track-label="trackLabel"
|
||||
@click="onDismiss"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<gl-sprintf :message="suggestContent" />
|
||||
<div class="mt-3">
|
||||
<gl-sprintf :message="suggestFooter">
|
||||
<template #bold="{ content }">
|
||||
<strong> {{ content }} </strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</gl-popover>
|
||||
</template>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Popover from './components/popover.vue';
|
||||
|
||||
export default (el) =>
|
||||
new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(Popover, {
|
||||
props: {
|
||||
target: el.dataset.target,
|
||||
trackLabel: el.dataset.trackLabel,
|
||||
dismissKey: el.dataset.dismissKey,
|
||||
mergeRequestPath: el.dataset.mergeRequestPath,
|
||||
humanAccess: el.dataset.humanAccess,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -1,36 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import { createAlert } from '~/alert';
|
||||
import { setCookie } from '~/lib/utils/common_utils';
|
||||
import Tracking from '~/tracking';
|
||||
import NewCommitForm from '../new_commit_form';
|
||||
|
||||
const initPopovers = () => {
|
||||
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
|
||||
|
||||
if (suggestEl) {
|
||||
const commitButton = document.querySelector('#commit-changes');
|
||||
if (commitButton) {
|
||||
const { dismissKey, humanAccess } = suggestEl.dataset;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mergeRequestPath = urlParams.get('mr_path') || true;
|
||||
|
||||
const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`;
|
||||
const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes';
|
||||
const commitTrackValue = '20';
|
||||
|
||||
commitButton.addEventListener('click', () => {
|
||||
setCookie(commitCookieName, mergeRequestPath);
|
||||
|
||||
Tracking.event(undefined, 'click_button', {
|
||||
label: commitTrackLabel,
|
||||
property: humanAccess,
|
||||
value: commitTrackValue,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const editBlobForm = $('.js-edit-blob-form');
|
||||
|
||||
|
|
@ -59,7 +30,6 @@ export default () => {
|
|||
isMarkdown,
|
||||
previewMarkdownPath,
|
||||
});
|
||||
initPopovers();
|
||||
})
|
||||
.catch((e) =>
|
||||
createAlert({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import VueApollo from 'vue-apollo';
|
|||
import VueRouter from 'vue-router';
|
||||
import { provideWebIdeLink } from 'ee_else_ce/pages/projects/shared/web_ide_link/provide_web_ide_link';
|
||||
import TableOfContents from '~/blob/components/table_contents.vue';
|
||||
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
|
||||
import { BlobViewer, initAuxiliaryViewer } from '~/blob/viewer/index';
|
||||
import GpgBadges from '~/gpg_badges';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
|
|
@ -191,22 +190,6 @@ if (codeNavEl && !viewBlobEl) {
|
|||
);
|
||||
}
|
||||
|
||||
const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
|
||||
|
||||
if (successPipelineEl) {
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: successPipelineEl,
|
||||
render(createElement) {
|
||||
return createElement(PipelineTourSuccessModal, {
|
||||
props: {
|
||||
...successPipelineEl.dataset,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const tableContentsEl = document.querySelector('.js-table-contents');
|
||||
|
||||
if (tableContentsEl) {
|
||||
|
|
|
|||
|
|
@ -30,40 +30,34 @@
|
|||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-session-tabs {
|
||||
display: flex;
|
||||
border-color: transparent;
|
||||
.new-session-tabs {
|
||||
display: flex;
|
||||
border-color: transparent;
|
||||
|
||||
.nav-item {
|
||||
border-bottom: 1px solid $gray-100;
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $gray-100;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav-link {
|
||||
width: 100%;
|
||||
font-size: 18px !important;
|
||||
|
||||
&.active {
|
||||
cursor: default;
|
||||
|
||||
li {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
cursor: default;
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.devise-errors {
|
||||
@include devise-errors;
|
||||
}
|
||||
.devise-errors {
|
||||
@include devise-errors;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ module AuthHelper
|
|||
end
|
||||
|
||||
def form_based_provider_priority
|
||||
['crowd', /^ldap/, 'kerberos']
|
||||
['crowd', /^ldap/]
|
||||
end
|
||||
|
||||
def form_based_provider_with_highest_priority
|
||||
|
|
|
|||
|
|
@ -300,20 +300,6 @@ module BlobHelper
|
|||
end
|
||||
end
|
||||
|
||||
def show_suggest_pipeline_creation_celebration?
|
||||
@project.ci_config_path_or_default == @blob.path &&
|
||||
@blob.auxiliary_viewer&.valid?(project: @project, sha: @commit.sha, user: current_user) &&
|
||||
cookies[suggest_pipeline_commit_cookie_name].present?
|
||||
end
|
||||
|
||||
def suggest_pipeline_commit_cookie_name
|
||||
"suggest_gitlab_ci_yml_commit_#{@project.id}"
|
||||
end
|
||||
|
||||
def human_access
|
||||
@project.team.human_max_access(current_user&.id).try(:downcase)
|
||||
end
|
||||
|
||||
def vue_blob_app_data(project, blob, ref)
|
||||
{
|
||||
blob_path: blob.path,
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SuggestPipelineHelper
|
||||
def should_suggest_gitlab_ci_yml?
|
||||
current_user && params[:suggest_gitlab_ci_yml] == 'true'
|
||||
end
|
||||
end
|
||||
|
|
@ -14,6 +14,7 @@ module TabHelper
|
|||
gl_tabs_classes = %w[nav gl-tabs-nav]
|
||||
|
||||
html_options = html_options.merge(
|
||||
role: 'tablist',
|
||||
class: [*html_options[:class], gl_tabs_classes].join(' ')
|
||||
)
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ module TabHelper
|
|||
end
|
||||
|
||||
html_options = html_options.merge(
|
||||
role: 'tab',
|
||||
class: [*html_options[:class], link_classes].join(' ')
|
||||
)
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ module TabHelper
|
|||
extra_tab_classes = html_options.delete(:tab_class)
|
||||
tab_class = %w[nav-item].push(*extra_tab_classes)
|
||||
|
||||
content_tag(:li, class: tab_class) do
|
||||
content_tag(:li, role: 'presentation', class: tab_class) do
|
||||
if block
|
||||
link_to(options, html_options, &block)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ module Ci
|
|||
end
|
||||
|
||||
def execute
|
||||
validate_catalog_resource
|
||||
create_version
|
||||
track_release_duration do
|
||||
validate_catalog_resource
|
||||
create_version
|
||||
end
|
||||
|
||||
if errors.empty?
|
||||
ServiceResponse.success
|
||||
|
|
@ -25,6 +27,20 @@ module Ci
|
|||
|
||||
attr_reader :project, :errors, :release
|
||||
|
||||
def track_release_duration
|
||||
name = :gitlab_ci_catalog_release_duration_seconds
|
||||
comment = 'CI Catalog Release duration'
|
||||
buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 240.0]
|
||||
|
||||
histogram = ::Gitlab::Metrics.histogram(name, comment, {}, buckets)
|
||||
start_time = ::Gitlab::Metrics::System.monotonic_time
|
||||
|
||||
yield
|
||||
|
||||
duration = ::Gitlab::Metrics::System.monotonic_time - start_time
|
||||
histogram.observe({}, duration.seconds)
|
||||
end
|
||||
|
||||
def validate_catalog_resource
|
||||
response = Ci::Catalog::Resources::ValidateService.new(project, release.sha).execute
|
||||
return if response.success?
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
= gl_tabs_nav({ class: 'new-session-tabs gl-border-0' }) do
|
||||
= gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1', data: { testid: 'sign-in-tab' } }
|
||||
= gl_tabs_nav({ class: 'nav-tabs nav-links new-session-tabs' }) do
|
||||
= gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tabindex: '-1', data: { testid: 'sign-in-tab' } }
|
||||
|
|
|
|||
|
|
@ -1,31 +1,28 @@
|
|||
- render_standard_signin = admin_mode ? allow_admin_mode_password_authentication_for_web? : password_authentication_enabled_for_web?
|
||||
|
||||
%ul.nav-links.new-session-tabs.nav-tabs.nav.nav-links-unboxed
|
||||
= gl_tabs_nav({ class: 'nav-tabs nav-links new-session-tabs' }) do
|
||||
- if crowd_enabled?
|
||||
%li.nav-item
|
||||
= link_to _("Crowd"), "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab', role: 'tab'
|
||||
= gl_tab_link_to _('Crowd'), '#crowd', { class: active_when(form_based_auth_provider_has_active_class?(:crowd)), data: { toggle: 'tab' } }
|
||||
|
||||
- ldap_servers.each_with_index do |server, i|
|
||||
%li.nav-item
|
||||
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', testid: 'ldap-tab' }, role: 'tab'
|
||||
= gl_tab_link_to server['label'], "##{server['provider_name']}", { class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)), data: { toggle: 'tab', testid: 'ldap-tab' } }
|
||||
|
||||
= render_if_exists 'devise/shared/tab_smartcard'
|
||||
|
||||
- if render_standard_signin
|
||||
%li.nav-item
|
||||
= link_to _('Standard'), '#login-pane', class: 'nav-link', data: { toggle: 'tab', testid: 'standard-tab' }, role: 'tab'
|
||||
= gl_tab_link_to _('Standard'), '#login-pane', { data: { toggle: 'tab', testid: 'standard-tab' } }
|
||||
|
||||
.tab-content
|
||||
- if crowd_enabled?
|
||||
.login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
|
||||
.tab-pane{ id: 'crowd', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
|
||||
= render 'devise/sessions/new_crowd', admin_mode: admin_mode
|
||||
|
||||
- ldap_servers.each_with_index do |server, i|
|
||||
.login-box.tab-pane{ id: server['provider_name'], role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) }
|
||||
.tab-pane{ id: server['provider_name'], role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) }
|
||||
= render 'devise/sessions/new_ldap', server: server, admin_mode: admin_mode
|
||||
|
||||
= render_if_exists 'devise/sessions/new_smartcard'
|
||||
|
||||
- if render_standard_signin
|
||||
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
|
||||
.tab-pane{ id: 'login-pane', role: 'tabpanel' }
|
||||
= render 'devise/sessions/new_base', admin_mode: admin_mode
|
||||
|
|
|
|||
|
|
@ -17,13 +17,8 @@
|
|||
= render 'filepath_form', input_options: input_options
|
||||
|
||||
- if current_action?(:new, :create)
|
||||
- input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : ''), required: true, placeholder: "Filename", testid: 'file-name-field', class: 'new-file-name js-file-path-name-input' }
|
||||
- input_options = { id: 'file_name', name: 'file_name', value: params[:file_name] || '', required: true, placeholder: "Filename", testid: 'file-name-field', class: 'new-file-name js-file-path-name-input' }
|
||||
= render 'filepath_form', input_options: input_options
|
||||
- if should_suggest_gitlab_ci_yml?
|
||||
.js-suggest-gitlab-ci-yml{ data: { track_label: 'suggest_gitlab_ci_yml',
|
||||
merge_request_path: params[:mr_path],
|
||||
dismiss_key: @project.id,
|
||||
human_access: human_access } }
|
||||
|
||||
- if Feature.enabled?(:source_editor_toolbar, current_user)
|
||||
#editor-toolbar
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
.js-success-pipeline-modal{ data: { 'commit-cookie': suggest_pipeline_commit_cookie_name,
|
||||
'go-to-pipelines-path': project_pipelines_path(@project),
|
||||
'project-merge-requests-path': project_merge_requests_path(@project),
|
||||
'example-link': help_page_path('ci/examples/index'),
|
||||
'code-quality-link': help_page_path('ci/testing/code_quality'),
|
||||
'human-access': @project.team.human_max_access(current_user&.id) } }
|
||||
|
|
@ -12,9 +12,3 @@
|
|||
= hidden_field_tag 'content', '', id: 'file-content'
|
||||
= render 'projects/commit_button', ref: @ref,
|
||||
cancel_path: project_tree_path(@project, @id)
|
||||
- if should_suggest_gitlab_ci_yml?
|
||||
.js-suggest-gitlab-ci-yml-commit-changes{ data: { target: '#commit-changes',
|
||||
merge_request_path: params[:mr_path],
|
||||
track_label: 'suggest_commit_first_project_gitlab_ci_yml',
|
||||
dismiss_key: @project.id,
|
||||
human_access: human_access } }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
#tree-holder.tree-holder.gl-pt-4
|
||||
= render 'blob', blob: @blob
|
||||
|
||||
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
|
||||
= render 'shared/web_ide_path'
|
||||
|
||||
-# https://gitlab.com/gitlab-org/gitlab/-/issues/408388#note_1578533983
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
window.gl.mrWidgetData.license_compliance_docs_path = '#{help_page_path('user/compliance/license_scanning_of_cyclonedx_files')}';
|
||||
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/approvals/rules.md', anchor: 'eligible-approvers')}';
|
||||
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/approvals/index.md")}';
|
||||
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/empty-state/empty-pipeline-md.svg')}';
|
||||
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
|
||||
window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}';
|
||||
window.gl.mrWidgetData.can_view_false_positive = '#{@merge_request.project.licensed_feature_available?(:sast_fp_reduction).to_s}';
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
description: "Show a congratulation on first pipeline"
|
||||
category: default
|
||||
action: generic
|
||||
label_description: "`congratulate_first_pipeline`"
|
||||
property_description: "`[admin | maintainer | developer | owner]`"
|
||||
value_description: ""
|
||||
extra_properties:
|
||||
identifiers:
|
||||
product_section: growth
|
||||
product_stage: growth
|
||||
product_group: group::expansion
|
||||
milestone: "12.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28378
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
description: "Go to pipeline on pipeline celebration"
|
||||
category: default
|
||||
action: click_button
|
||||
label_description: "`congratulate_first_pipeline`"
|
||||
property_description: "`[admin | maintainer | developer | owner]`"
|
||||
value_description: "`10`"
|
||||
extra_properties:
|
||||
identifiers:
|
||||
product_section: growth
|
||||
product_stage: growth
|
||||
product_group: group::expansion
|
||||
milestone: "12.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28378
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
description: "Dismiss GitLab CI suggestion popover"
|
||||
category: default
|
||||
action: click_button
|
||||
label_description: "[ `suggest_commit_first_project_gitlab_ci_yml` ]"
|
||||
property_description: "`[admin | maintainer | developer | owner]`"
|
||||
value_description: "`10`"
|
||||
extra_properties:
|
||||
identifiers:
|
||||
product_section: growth
|
||||
product_stage: growth
|
||||
product_group: group::expansion
|
||||
milestone: "12.10"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26105
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: arkose_labs_trial_signup_challenge
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113985
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395754
|
||||
milestone: '15.10'
|
||||
type: development
|
||||
group: group::anti-abuse
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: use_primary_and_secondary_stores_for_repository_cache
|
||||
feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2854
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144548
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442163
|
||||
milestone: '16.10'
|
||||
group: group::scalability
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: use_primary_store_as_default_for_repository_cache
|
||||
feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2854
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144548
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442163
|
||||
milestone: '16.10'
|
||||
group: group::scalability
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -4,7 +4,7 @@ key_path: redis_hll_counters.ci_templates.p_ci_templates_5_min_production_app_mo
|
|||
description: Number of projects using 5 min production app CI template in last 7 days.
|
||||
product_section: seg
|
||||
product_stage: deploy
|
||||
product_group: five_min_app
|
||||
product_group: 5-min-app
|
||||
value_type: number
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ key_path: redis_hll_counters.ci_templates.p_ci_templates_5_min_production_app_we
|
|||
description: Number of projects using 5 min production app CI template in last 7 days.
|
||||
product_section: seg
|
||||
product_stage: deploy
|
||||
product_group: five_min_app
|
||||
product_group: 5-min-app
|
||||
value_type: number
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@
|
|||
]
|
||||
},
|
||||
"product_group": {
|
||||
"type": "string",
|
||||
"pattern": "^$|^([a-z]+_)*[a-z]+$"
|
||||
"type": "string"
|
||||
},
|
||||
"value_type": {
|
||||
"type": "string",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
status: ongoing
|
||||
creation-date: "2024-01-29"
|
||||
authors: [ "@jarv" ]
|
||||
coach:
|
||||
approvers: [ ]
|
||||
---
|
||||
|
||||
# Disaster Recovery
|
||||
|
||||
This document is a work-in-progress and proposes architecture changes for the GitLab.com SaaS.
|
||||
The goal of these changes are to maintain GitLab.com service continuity in the case a regional or zonal outage.
|
||||
|
||||
- A **zonal recovery** is required when all resources are unavailable in one of the three availability zones in `us-east1` or `us-central1`.
|
||||
- A **regional recovery** is required when all resources become unavailable in one of the regions critical to operation of GitLab.com, either `us-east1` or `us-central1`.
|
||||
|
||||
## Services not included in the current DR strategy for FY24 and FY25
|
||||
|
||||
We have limited the scope of DR to services that support primary services (Web, API, Git, Pages, Sidekiq, CI, and Registry).
|
||||
These services tie directly into our overall [availability score](https://dashboards.gitlab.net/d/general-slas/general3a-slas?orgId=1) (internal link) for GitLab.com.
|
||||
|
||||
For example, DR does not include the following:
|
||||
|
||||
- AI services including code suggestions
|
||||
- Error tracking and other observability services like tracing
|
||||
- CustomersDot, responsible for billing and new subscriptions
|
||||
- Advanced Search
|
||||
|
||||
## DR Implementation Targets
|
||||
|
||||
The FY24 targets were:
|
||||
|
||||
| | Recovery Time Objective (RTO) | Recovery Point Objective (RPO) |
|
||||
|--------------|-------------------------------|--------------------------------|
|
||||
| **Zonal** | 2 hours | 1 hour |
|
||||
| **Regional** | 96 hours | 2 hours |
|
||||
|
||||
The FY25 targets before cell architecture are:
|
||||
|
||||
| | Recovery Time Objective (RTO) | Recovery Point Objective (RPO) |
|
||||
|--------------|-------------------------------|--------------------------------|
|
||||
| **Zonal** | 0 minutes | 0 minutes |
|
||||
| **Regional** | 48 hours | 0 minutes |
|
||||
|
||||
## Current Recovery Time Objective (RTO) and Recovery Point Objective (RPO) for Zonal Recovery
|
||||
|
||||
We have not yet simulated a full zonal outage on GitLab.com.
|
||||
The following are RTO/RPO estimates based on what we have been able to test using the [disaster recovery runbook](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/disaster-recovery?ref_type=heads).
|
||||
It is assumed that each service can be restored in parallel.
|
||||
A parallel restore is the only way we are able to meet the FY24 RTO target of 2 hours for a zonal recovery.
|
||||
|
||||
| Service | RTO | RPO |
|
||||
| --- | --- | --- |
|
||||
| PostgreSQL | 1.5 hr | <=5 min |
|
||||
| Redis [^1] | 0 | 0 |
|
||||
| Gitaly | 30 min | <=1 hr |
|
||||
| CI | 30 min | not applicable |
|
||||
| Load balancing (HAProxy) | 30 min | not applicable |
|
||||
| Frontend services (Web, API, Git, Pages, Registry) [^2] | 15 min | 0 |
|
||||
| Monitoring (Prometheus, Thanos, Grafana, Alerting) | 0 | not applicable |
|
||||
| Operations (Deployments, runbooks, operational tooling, Chef) [^3] | 30 min | 4 hr |
|
||||
| PackageCloud (distribution of packages for self-managed) | 0 | 0 |
|
||||
|
||||
## Current Recovery Time Objective (RTO) and Recovery Point Objective (RPO) for Regional Recovery
|
||||
|
||||
Regional recovery requires a complete rebuild of GitLab.com using backups that are stored in multi-region buckets.
|
||||
The recovery has not yet been validated end-to-end, so we don't know how long the RTO is for a regional failure.
|
||||
Our target RTO for FY25 is to have a procedure to recover from a regional outage in under 48 hours.
|
||||
|
||||
The following are considerations for choosing multi-region buckets over dual-region buckets:
|
||||
|
||||
- We operate out of a single region so multi-region storage is only used for disaster recovery.
|
||||
- Although Google recommends dual-region for disaster recovery, dual-region is [not an available storage type for disk snapshots](https://cloud.google.com/compute/docs/disks/snapshots#selecting_a_storage_location).
|
||||
- To mitigate the bandwidth limitation of multi-region buckets, we spread Gitaly VMs infra across multiple projects.
|
||||
|
||||
## Proposals for Regional and Zonal Recovery
|
||||
|
||||
- [Regional](regional.md)
|
||||
- [Zonal](zonal.md)
|
||||
|
||||
---
|
||||
|
||||
[^1]: Most of the Redis load is on the primary node, so losing replicas should not cause any service interruption
|
||||
[^2]: We setup maximum replicas in our Kubernetes clusters servicing front-end traffic, this is done to avoid saturating downstream dependencies. For a zonal failure, a cluster reconfiguration is necessary to increase these maximums.
|
||||
[^3]: There is a 4 hr RPO for Operations because Chef is an single point of failure in a single availability zone and our restore method uses disk snapshots, taken every 4 hours. While most of our Chef configuration is also stored in Git, some data (like node registrations) are only stored on the server.
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
status: ongoing
|
||||
creation-date: "2024-01-29"
|
||||
authors: [ "@jarv" ]
|
||||
coach:
|
||||
approvers: [ ]
|
||||
---
|
||||
|
||||
# Regional Recovery
|
||||
|
||||
## Improving the Recovery Time Objective (RTO) and Recovery Point Objective (RPO) for Regional Recovery
|
||||
|
||||
The following list the top challenges that limit our ability to drive `RTO` to 48 hours for a regional recovery.
|
||||
|
||||
1. We have a large amount of legacy infrastructure managed using Chef. This configuration has been difficult for us to manage and would require a large a mount of manual copying and duplication to create new infrastructure in an alternate region.
|
||||
1. Operational infrastructure is located in a single region, `us-central1`. For a regional failure in this region, it requires rebuilding the ops infrastructure with only local copies of runbooks and tooling scripts.
|
||||
1. Observability is hosted in a single region.
|
||||
1. The infrastructure (`dev.gitlab.org`) that builds Docker images and packages is located in a single region, and is a single point of failure.
|
||||
1. There is no launch-pad that would allow us to get a head-start on a regional recovery. Our IaC (Infrastructure-as-Code) does not allow us to switch regions for provisioning.
|
||||
1. We don't have confidence that Google can provide us with the capacity we need in a new region, specifically the large amount of SSD necessary to restore all of our customer Git data.
|
||||
1. We use [Global DNS](https://cloud.google.com/compute/docs/internal-dns) for internal DNS making it difficult to use multiple instances with the same name across multiple regions, we also don't incorporate regions into DNS names for our internal endpoints (for example dashboards, logs, etc).
|
||||
1. If we deploy replicas in another region to reduce RPO we are not yet sure of the latency or cloud spend impacts.
|
||||
1. We have special/negotiated Quota increases for Compute, Network, and API with the Google Cloud Platform only for a single region, we have to match these quotas in a new region, and keep them in sync.
|
||||
1. We have not standardized a way to divert traffic at the edge from 1 region to another.
|
||||
1. In monitoring, and configuration we have places where we hardcode the region to `us-east1`.
|
||||
|
||||
## Regional recovery work-streams
|
||||
|
||||
The first step of our regional recovery plan creates new infrastructure in the recovery region that involves a large number of manual steps.
|
||||
To give us a head-start on recovery, we propose a "regional bulkhead" deployment in a new GCP region.
|
||||
|
||||
A "regional bulkhead" meets the following requirements:
|
||||
|
||||
1. A specific region is allocated.
|
||||
1. Quotas are set and synced so that we can duplicate all of us-east1 in the new region.
|
||||
1. Subnets are allocated or reserved in the same VPC for us-east1.
|
||||
1. Some infrastructure is deployed where it makes sense to lower RTO, while keeping cloud-spend low.
|
||||
|
||||
The following are work-streams that can be done mostly in parallel.
|
||||
The end-goal of the regional recovery is to have a bulkhead that has the basic scaffolding for deployment in the alternate region.
|
||||
This bulkhead can be used as a launching pad for a full data restore from `us-east1` to the alternate region.
|
||||
|
||||
### Select an alternate region
|
||||
|
||||
- Dependencies: none
|
||||
- Teams: Ops
|
||||
|
||||
The following are considerations that need to be made when selecting an alternate region for DR:
|
||||
|
||||
1. Ensure there is enough capacity to meet compute usage.
|
||||
1. Network and network latency requirements, if any.
|
||||
1. Feature parity between regions.
|
||||
|
||||
### Deploy Kubernetes clusters supporting front-end services in a new region with deployments
|
||||
|
||||
- Dependencies: [External front-end load balancing](#external-front-end-load-balancing)
|
||||
- Teams: Ops, Foundations, Delivery
|
||||
|
||||
GitLab.com has Web, API, Git, Git HTTPs, Git SSH, Pages, and Registry as front-end services.
|
||||
All of these services are run in 4 Kubernetes clusters deployed in `us-east1`.
|
||||
These services are either stateless or use multi-region storage buckets for data.
|
||||
In the case of a failure in `us-east1`, we would need to rebuild these clusters in the alternate region and set them up for deployments.
|
||||
|
||||
### Switch from Global to Zonal DNS
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Gitaly
|
||||
|
||||
Gitaly VMs are single points of failure that are deployed in `us-east1`.
|
||||
The internal DNS naming of the nodes have the following convention:
|
||||
|
||||
```plaintext
|
||||
gitaly-01-stor-gprd.c.gitlab-gitaly-gprd-ccb0.internal
|
||||
^ name ^ project
|
||||
```
|
||||
|
||||
By switching to zonal DNS, we can change the internal DNS entries so they have the zone in the DNS name:
|
||||
|
||||
```plaintext
|
||||
gitaly-01-stor-gprd.c.us-east1-b.gitlab-gitaly-gprd-ccb0.internal
|
||||
^ name ^ zone ^ project
|
||||
```
|
||||
|
||||
Allowing us to keep the same name when recovering into a new region or zone.
|
||||
|
||||
```plaintext
|
||||
gitaly-01-stor-gprd.c.us-east1-b.gitlab-gitaly-gprd-ccb0.internal
|
||||
gitaly-01-stor-gprd.c.us-east4-a.gitlab-gitaly-gprd-ccb0.internal
|
||||
```
|
||||
|
||||
For fleets of VMs outside of Kubernetes, these names allow us to have the same node names in the recovery region.
|
||||
|
||||
### Gitaly
|
||||
|
||||
- Dependencies: [Switch from Global to Zonal DNS](#switch-from-global-to-zonal-dns) (optional, but desired)
|
||||
- Teams: Gitaly, Ops, Foundations
|
||||
|
||||
Restoring the entire Gitaly fleet requires a large number of VMs deployed in the alternate region.
|
||||
It also requires a lot of bandwidth because restore is based on disk snapshots.
|
||||
To ensure a successful Gitaly restore, quotas need to be synced with us-east1 and there needs to be end-to-end validation.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
- Dependencies: [Improve Chef provisioning time by using preconfigured golden OS images](zonal.md#improve-chef-provisioning-time-by-using-preconfigured-golden-os-images) (optional, but desired), local backups in the standby region (data disk snapshot and `WAL` archiving).
|
||||
- Teams: Database Reliability, Ops
|
||||
|
||||
The configuration for Patroni provisioning only allows a single region per cluster.
|
||||
There is networking infrastructure, Consul, and load balancers that need to be setup in the alternate region.
|
||||
We may consider setting up a "cascaded cluster" for the databases to improve recovery time for replication.
|
||||
|
||||
### Redis
|
||||
|
||||
- Dependencies: [Improve Chef provisioning time by using preconfigured golden OS images](zonal.md#improve-chef-provisioning-time-by-using-preconfigured-golden-os-images) (optional, but desired)
|
||||
- Teams: Ops
|
||||
|
||||
To provision Redis subnets need to be allocated in the alternate region with and end-to-end validation of the new deployments.
|
||||
|
||||
### External front-end load balancing
|
||||
|
||||
- Dependencies: HAProxy replacement, mostly likely [GKE Gateway and Istio](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1157)
|
||||
- Teams: Ops, Foundations
|
||||
|
||||
External front-end load balancing is necessary to validate the deployment in the alternate region.
|
||||
This requires both external and internal LBs for all front-end-services.
|
||||
|
||||
### Monitoring
|
||||
|
||||
- Dependencies: [Eliminate X% Chef dependencies in Infra by moving infra away from Chef](zonal.md#eliminate-x-chef-dependencies-in-infra-by-moving-infra-away-from-chef) (migrate Prometheus infra to Kubernetes)
|
||||
- Teams: Scalability:Observability, Ops, Foundations
|
||||
|
||||
Setup an alternate ops Kubernetes cluster in a different region that is scaled down to zero replicas.
|
||||
|
||||
### Runners
|
||||
|
||||
Dependencies: [Improve Chef provisioning time by using preconfigured golden OS images](zonal.md#improve-chef-provisioning-time-by-using-preconfigured-golden-os-images) (optional, but desired)
|
||||
Teams: Scalability:Practices, Ops, Foundations
|
||||
|
||||
Ensure quotas are set and align with us-east1 in the alternate region for both runner managers and ephemeral VMs.
|
||||
Setup and validate networking configuration with peering configuration.
|
||||
|
||||
### Ops and Packaging
|
||||
|
||||
- Dependencies: [Create an HA Chef server configuration to avoid an outage for a single zone failure](zonal.md#create-an-ha-chef-server-configuration-to-avoid-an-outage-for-a-single-zone-failure)
|
||||
- Teams: Scalability:Practices, Ops, Foundations, Distribution
|
||||
|
||||
All image creation and packaging is done on a single VM, our operation tooling is also on a single VM.
|
||||
Both of these are single points of failures that have data stored locally.
|
||||
In the case of a regional outage, we would need to rebuild them from snapshot and lose about 4 hours of data.
|
||||
|
||||
The following are options to mitigate this risk:
|
||||
|
||||
- Move our packaging jobs to `ops.gitlab.net` so we eliminate `dev.gitlab.org` as a single point of failure.
|
||||
- Use the Geo feature for `ops.gitlab.net`.
|
||||
|
||||
### Regional Recovery Gameday
|
||||
|
||||
- Dependencies: Recovery improvements
|
||||
- Teams: Ops
|
||||
|
||||
Following the improvements for regional recovery, a Gameday needs to be executed for end-to-end testing of the procedure.
|
||||
Once validated, it can be added to our existing [disaster recovery runbook](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/disaster-recovery?ref_type=heads).
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
status: ongoing
|
||||
creation-date: "2024-01-29"
|
||||
authors: [ "@jarv" ]
|
||||
coach:
|
||||
approvers: [ ]
|
||||
---
|
||||
|
||||
# Zonal Recovery
|
||||
|
||||
## Improving the Recovery Time Objective (RTO) and Recovery Point Objective (RPO) for Zonal Recovery
|
||||
|
||||
The following represents our current DR challenges and are candidates for problems that we should address in this architecture blueprint.
|
||||
|
||||
1. Postgres replicas run close to capacity and are scaled manually. New instances must go through Terraform CI pipelines and Chef configuration. Over-provisioning to absorb a zone failure would add significant cloud-spend (see proposal section at the end of the document for details).
|
||||
1. HAProxy (load balancing) is scaled manually and must go through Terraform CI pipelines and Chef configuration.
|
||||
1. CI runner managers are present in 2 availability zones and scaled close to capacity. New instances must go through Terraform CI pipelines and Chef configuration.
|
||||
1. In a zone there are saturation limits, like the number of replicas that need to be manually adjusted if load is shifted away from a failed availability zone.
|
||||
1. Gitaly `RPO` is limited by the frequency of disk snapshots, `RTO` is limited by the time it takes to provision and configure through Terraform CI pipelines and Chef configuration.
|
||||
1. Monitoring infrastructure that collects metrics from Chef managed VMs is redundant across 2 availability zones and scaled manually. New instances must go through Terraform CI pipelines and Chef configuration.
|
||||
1. The Chef server which is responsible for all configuration of Chef managed VMs is a single point of failure located in `us-central1`. It has a local Postgres database and files on local disk.
|
||||
1. The infrastructure (`dev.gitlab.org`) that builds Docker images and packages is located in a single region, and is a single point of failure.
|
||||
|
||||
## Zonal recovery work-streams
|
||||
|
||||
Improvements around zonal recovery revolve around improving the time it takes to provision for fleets that do not automatically scale.
|
||||
There is already work in-progress to completely eliminate statically allocated VMs like HAProxy.
|
||||
Additionally efforts can be made to shorten launch and configuration times for fleets that are not able to automatically scale like Gitaly, PostgreSQL and Redis.
|
||||
|
||||
### Over-provision to absorb a single zone failure
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Ops, Scalability:Practices, Database Reliability
|
||||
|
||||
All of our Chef managed VM fleets run close to capacity and require manual scaling and provisioning using Terraform/Chef.
|
||||
In the case of a zonal outage, it is necessary to provision more servers through Terraform which adds to our recovery time objective.
|
||||
One way to avoid this is to over-provision so we have a full zone's worth of extra capacity.
|
||||
|
||||
1. Patroni Main (`n2-highmem-128` 6.5k/month): 3 additional nodes for +20k/month
|
||||
1. Patroni CI (`n2-highmem-96` 5k/month): 3 additional nodes for +15k/month
|
||||
1. HAProxy (`t2d-standard-8` 285/month): 20 additional nodes for +5k/month
|
||||
1. CI Runner managers (`c2-standard-30` 1.3k/month) 60 additional nodes for +78k/month
|
||||
|
||||
The Kubernetes horizontal auto-scaler (`HPA`) has a maximum number of pods configured on front-end services.
|
||||
It is configured to protect downstream dependencies like the database from saturation due to scaling events.
|
||||
If we allow a zone to scale up rapidly, these limits need to be adjusted or re-evaluated in the context of disaster recovery.
|
||||
|
||||
### Remove HAProxy as a load balancing layer
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Foundations
|
||||
|
||||
HAProxy is a fleet of Chef managed VMs that are statically allocated across 3 AZs in `us-east1`.
|
||||
In the case of a zonal outage we would need to rapidly scale this fleet, adding to our RTO.
|
||||
|
||||
In FY24Q4 the Foundations team started working on a proof-of-concept to use [Istio in non-prod environments](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1157).
|
||||
We anticipate in FY25 to have a replacement for HAProxy using Istio and [GKE Gateway](https://cloud.google.com/kubernetes-engine/docs/concepts/gateway-api).
|
||||
Completing this work reduces the impact to our LoadBalancing layer for zonal outages, as it eliminates the need to manually scale the HAProxy fleet.
|
||||
Additionally, we spend around 17k/month on HAProxy nodes, so there may be a cloud-spend reduction if we are able to reduce this footprint.
|
||||
|
||||
### Create an HA Chef server configuration to avoid an outage for a single zone failure
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Ops
|
||||
|
||||
Chef is responsible for configuring VMs that have workloads outside of Kubernetes.
|
||||
It is a single point of failure that resides in `us-central1-b`.
|
||||
Data is persisted locally on disk, and we have not yet investigated moving it to a highly available setup.
|
||||
In the case of a zonal outage of `us-central1-b` the server would need to be rebuilt from snapshot, losing up to 4 hours of data.
|
||||
|
||||
### Create an HA Packaging server (`dev.gitlab.org`) configuration to avoid an outage for a single zone failure
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Ops
|
||||
|
||||
In the case of a zonal outage of `us-east1-c` the server would need to be rebuilt from snapshot, losing up to 4 hours of data.
|
||||
The additional challenge of this host is that it is a GitLab-CE instance so we would be limited in features.
|
||||
The best approach here would likely be to move packaging CI pipelines to `ops.gitlab.net`.
|
||||
|
||||
### Improve Chef provisioning time by using preconfigured golden OS images
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Ops
|
||||
|
||||
For the [Gitaly fleet upgrade in 2022](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/601) a scheduled CI pipeline was created to build a golden OS images.
|
||||
We can revive this work and start generating images for Gitaly and other VMs to shorten configuration time.
|
||||
We estimate that using an image can reduce our recovery time by about 15 minutes to improve RTO for zonal failures.
|
||||
|
||||
### Eliminate X% Chef dependencies in Infra by moving infra away from Chef
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Ops, Scalability:Observability, Scalability:Practices
|
||||
|
||||
Gitaly, Postgres, CI runner managers, HAProxy, Bastion, CustomersDot, Deploy, DB Lab, Prometheus, Redis, SD Exporter Sentry, and Console servers are managed by Chef.
|
||||
To help improve the speed of recoveries, we can move this infrastructure into Kubernetes or Ansible for configuration management.
|
||||
|
||||
### Write-ahead-log for Gitaly snapshot restores
|
||||
|
||||
- Dependencies: None
|
||||
- Teams: Gitaly
|
||||
|
||||
There is [work planned in FY25Q1](https://gitlab.com/gitlab-com/gitlab-OKRs/-/work_items/5710) that adds a transaction log for Gitaly to reduce RPO.
|
||||
|
|
@ -254,7 +254,7 @@ Code Quality can be customized by defining available CI/CD variables:
|
|||
| `ENGINE_MEMORY_LIMIT_BYTES` | Set the memory limit for engines. Default: 1,024,000,000 bytes. |
|
||||
| `REPORT_STDOUT` | Set to print the report to `STDOUT` instead of generating the usual report file. |
|
||||
| `REPORT_FORMAT` | Set to control the format of the generated report file. Either `json` or `html`. |
|
||||
| `SOURCE_CODE` | Path to the source code to scan. |
|
||||
| `SOURCE_CODE` | Path to the source code to scan. Must be the absolute path to a directory where cloned sources are stored. |
|
||||
| `TIMEOUT_SECONDS` | Custom timeout per engine container for the `codeclimate analyze` command. Default: 900 seconds (15 minutes) |
|
||||
|
||||
## Output
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ Before you enable these features, ensure [hard email confirmation](../security/u
|
|||
| `identity_verification_phone_number` | Turns on phone verification for medium risk users for all flows (the Arkose challenge flag for the specific flow and the `identity_verification` flag must be enabled for this to have effect) |
|
||||
| `identity_verification_credit_card` | Turns on credit card verification for high risk users for all flows (the Arkose challenge flag for the specific flow and the `identity_verification` flag must be enabled for this to have effect) |
|
||||
| `arkose_labs_signup_challenge` | Enables Arkose challenge for all flows, except the Trial and OAuth flows |
|
||||
| `arkose_labs_trial_signup_challenge` | Enables Arkose challenge for the Trial flow (the `arkose_labs_signup_challenge` flag must be enabled as well for this to have effect) |
|
||||
| `arkose_labs_oauth_signup_challenge` | Enables Arkose challenge for the OAuth flow |
|
||||
|
||||
## Logging
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ Alerts are a critical entity in your incident management workflow. They represen
|
|||
|
||||
## Alert list
|
||||
|
||||
Users with at least the Developer role can
|
||||
access the Alert list at **Monitor > Alerts** in your project's
|
||||
sidebar. The Alert list displays alerts sorted by start time, but
|
||||
you can change the sort order by selecting the headers in the Alert list.
|
||||
Users with at least the Developer role can access the Alert list at **Monitor > Alerts** in your project's sidebar. The Alert list displays alerts sorted by start time, but you can change the sort order by selecting the headers in the Alert list.
|
||||
|
||||
The alert list displays the following information:
|
||||
|
||||
|
|
@ -43,9 +40,7 @@ The alert list displays the following information:
|
|||
|
||||
## Alert severity
|
||||
|
||||
Each level of alert contains a uniquely shaped and color-coded icon to help
|
||||
you identify the severity of a particular alert. These severity icons help you
|
||||
immediately identify which alerts you should prioritize investigating:
|
||||
Each level of alert contains a uniquely shaped and color-coded icon to help you identify the severity of a particular alert. These severity icons help you immediately identify which alerts you should prioritize investigating:
|
||||
|
||||

|
||||
|
||||
|
|
@ -66,14 +61,9 @@ Alerts contain one of the following icons:
|
|||
|
||||
## Alert details page
|
||||
|
||||
Navigate to the Alert details view by visiting the [Alert list](alerts.md)
|
||||
and selecting an alert from the list. You need at least the Developer role
|
||||
to access alerts.
|
||||
for this demo project. Select any alert in the list to examine its alert details
|
||||
page.
|
||||
Navigate to the Alert details view by visiting the [Alert list](alerts.md) and selecting an alert from the list. You need at least the Developer role to access alerts. Select any alert in the list to examine its alert details page.
|
||||
|
||||
Alerts provide **Overview** and **Alert details** tabs to give you the right
|
||||
amount of information you need.
|
||||
Alerts provide **Overview** and **Alert details** tabs to give you the right amount of information you need.
|
||||
|
||||
### Alert details tab
|
||||
|
||||
|
|
@ -84,8 +74,7 @@ The **Alert details** tab has two sections. The top section provides a short lis
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217768) in GitLab 13.2.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/340852) in GitLab 14.10. In GitLab 14.9 and earlier, this tab shows a metrics chart for alerts coming from Prometheus.
|
||||
|
||||
In many cases, alerts are associated to metrics. You can upload screenshots of metric
|
||||
charts in the **Metrics** tab.
|
||||
In many cases, alerts are associated to metrics. You can upload screenshots of metric charts in the **Metrics** tab.
|
||||
|
||||
To do so, either:
|
||||
|
||||
|
|
@ -102,8 +91,7 @@ If you add a link, it is shown above the uploaded image.
|
|||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab 13.1.
|
||||
|
||||
The **Activity feed** tab is a log of activity on the alert. When you take action on an alert, this is logged as a system note. This gives you a linear
|
||||
timeline of the alert's investigation and assignment history.
|
||||
The **Activity feed** tab is a log of activity on the alert. When you take action on an alert, this is logged as a system note. This gives you a linear timeline of the alert's investigation and assignment history.
|
||||
|
||||
The following actions result in a system note:
|
||||
|
||||
|
|
@ -144,8 +132,7 @@ To change an alert's status:
|
|||
1. On the right sidebar, select **Edit**.
|
||||
1. Select a status.
|
||||
|
||||
To stop email notifications for alert recurrences in projects with [email notifications enabled](paging.md#email-notifications-for-alerts),
|
||||
change the alert's status away from **Triggered**.
|
||||
To stop email notifications for alert recurrences in projects with [email notifications enabled](paging.md#email-notifications-for-alerts), change the alert's status away from **Triggered**.
|
||||
|
||||
#### Resolve an alert by closing the linked incident
|
||||
|
||||
|
|
@ -153,9 +140,7 @@ Prerequisites:
|
|||
|
||||
- You must have at least the Reporter role.
|
||||
|
||||
When you [close an incident](manage_incidents.md#close-an-incident) that is linked to an alert,
|
||||
GitLab [changes the alert's status](#change-an-alerts-status) to **Resolved**.
|
||||
You are then credited with the alert's status change.
|
||||
When you [close an incident](manage_incidents.md#close-an-incident) that is linked to an alert, GitLab [changes the alert's status](#change-an-alerts-status) to **Resolved**. You are then credited with the alert's status change.
|
||||
|
||||
#### As an on-call responder
|
||||
|
||||
|
|
@ -163,8 +148,7 @@ DETAILS:
|
|||
**Tier:** Premium, Ultimate
|
||||
**Offering:** SaaS, self-managed
|
||||
|
||||
On-call responders can respond to [alert pages](paging.md#escalating-an-alert)
|
||||
by changing the alert status.
|
||||
On-call responders can respond to [alert pages](paging.md#escalating-an-alert) by changing the alert status.
|
||||
|
||||
Changing the status has the following effects:
|
||||
|
||||
|
|
@ -172,16 +156,13 @@ Changing the status has the following effects:
|
|||
- To **Resolved**: silences all on-call pages for the alert.
|
||||
- From **Resolved** to **Triggered**: restarts the alert escalating.
|
||||
|
||||
In GitLab 15.1 and earlier, updating the status of an [alert with an associated incident](manage_incidents.md#from-an-alert)
|
||||
also updates the incident status. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057),
|
||||
the incident status is independent and does not update when the alert status changes.
|
||||
In GitLab 15.1 and earlier, updating the status of an [alert with an associated incident](manage_incidents.md#from-an-alert) also updates the incident status. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), the incident status is independent and does not update when the alert status changes.
|
||||
|
||||
### Assign an alert
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab 13.1.
|
||||
|
||||
In large teams, where there is shared ownership of an alert, it can be
|
||||
difficult to track who is investigating and working on it. Assigning alerts eases collaboration and delegation by indicating which user is owning the alert. GitLab supports only a single assignee per alert.
|
||||
In large teams, where there is shared ownership of an alert, it can be difficult to track who is investigating and working on it. Assigning alerts eases collaboration and delegation by indicating which user is owning the alert. GitLab supports only a single assignee per alert.
|
||||
|
||||
To assign an alert:
|
||||
|
||||
|
|
@ -201,16 +182,13 @@ To assign an alert:
|
|||
From the list, select each user you want to assign to the alert.
|
||||
GitLab creates a [to-do item](../../user/todos.md) for each user.
|
||||
|
||||
After completing their portion of investigating or fixing the alert, users can
|
||||
unassign themselves from the alert. To remove an assignee, select **Edit** next to the **Assignee** dropdown list
|
||||
and clear the user from the list of assignees, or select **Unassigned**.
|
||||
After completing their portion of investigating or fixing the alert, users can unassign themselves from the alert. To remove an assignee, select **Edit** next to the **Assignee** dropdown list and clear the user from the list of assignees, or select **Unassigned**.
|
||||
|
||||
### Create a to-do item from an alert
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab 13.1.
|
||||
|
||||
You can manually create a [to-do item](../../user/todos.md) for yourself
|
||||
from an alert, and view it later on your **To-Do List**.
|
||||
You can manually create a [to-do item](../../user/todos.md) for yourself from an alert, and view it later on your **To-Do List**.
|
||||
|
||||
To add a to-do item, on the right sidebar, select **Add a to do**.
|
||||
|
||||
|
|
|
|||
|
|
@ -51,12 +51,11 @@ GitLab checks certificate revocation lists on a daily basis with a background wo
|
|||
`subjectKeyIdentifier`, and `crlDistributionPoints` display as **Unverified**. We
|
||||
recommend using certificates from a PKI that are in line with
|
||||
[RFC 5280](https://www.rfc-editor.org/rfc/rfc5280).
|
||||
- If you have more than one email in the Subject Alternative Name list in
|
||||
- In GitLab 16.2 and earlier, if you have more than one email in the Subject Alternative Name list in
|
||||
your signing certificate,
|
||||
[only the first one is used to verify commits](https://gitlab.com/gitlab-org/gitlab/-/issues/336677).
|
||||
- The `X509v3 Subject Key Identifier` (SKI) in the issuer certificate and the
|
||||
signing certificate
|
||||
[must be 40 characters long](https://gitlab.com/gitlab-org/gitlab/-/issues/332503).
|
||||
- In GitLab 15.1 and earlier, the `X509v3 Subject Key Identifier` (SKI) in the issuer certificate and the
|
||||
signing certificate [must be 40 characters long](https://gitlab.com/gitlab-org/gitlab/-/issues/332503).
|
||||
If your SKI is shorter, commits don't show as verified in GitLab, and
|
||||
short subject key identifiers may also
|
||||
[cause errors when accessing the project](https://gitlab.com/gitlab-org/gitlab/-/issues/332464),
|
||||
|
|
@ -254,9 +253,8 @@ To investigate why a commit shows as `Unverified`:
|
|||
sigemail == commitemail
|
||||
```
|
||||
|
||||
A [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336677) exists:
|
||||
only the first email in the `Subject Alternative Name` list is compared. To
|
||||
display the `Subject Alternative Name` list, run:
|
||||
In GitLab 16.2 and earlier, [only the first email](https://gitlab.com/gitlab-org/gitlab/-/issues/336677)
|
||||
in the `Subject Alternative Name` list is compared. To display the `Subject Alternative Name` list, run:
|
||||
|
||||
```ruby
|
||||
signature.__send__ :get_certificate_extension,'subjectAltName'
|
||||
|
|
@ -347,12 +345,32 @@ step of the previous [main verification checks](#main-verification-checks).
|
|||
1. After adding more certificates, (if these troubleshooting steps then pass)
|
||||
run the Rake task to [re-verify commits](#re-verify-commits).
|
||||
|
||||
1. Display the certificates, including in the signature:
|
||||
1. You can add additional certificates dynamically in the Rails console to check
|
||||
if this resolves the problem.
|
||||
|
||||
1. Retest the signature with a trust store `cert_store` that can be modified.
|
||||
It should still fail, with `false`:
|
||||
|
||||
```ruby
|
||||
cert_store = signature.__send__ :cert_store
|
||||
signature.__send__(:p7).verify([], cert_store, signature.__send__(:signed_text))
|
||||
```
|
||||
|
||||
1. Add an additional certificate, and re-test:
|
||||
|
||||
```ruby
|
||||
cert_store.add_file("/etc/ssl/certs/my_new_root_ca.pem")
|
||||
signature.__send__(:p7).verify([], cert_store, signature.__send__(:signed_text))
|
||||
```
|
||||
|
||||
1. Display the certificates that are included in the signature:
|
||||
|
||||
```ruby
|
||||
pp signature.__send__(:p7).certificates ; nil
|
||||
```
|
||||
|
||||
1. [Further investigation can be performed with OpenSSL on the command line](#smime-verification-with-openssl).
|
||||
|
||||
Ensure any additional intermediate certificates and the root certificate are added
|
||||
to the certificate store. For consistency with how certificate chains are built on
|
||||
web servers:
|
||||
|
|
@ -364,3 +382,144 @@ web servers:
|
|||
If you remove a root certificate from the GitLab
|
||||
trust store, such as when it expires, commit signatures which chain back to that
|
||||
root display as `unverified`.
|
||||
|
||||
#### S/MIME verification with OpenSSL
|
||||
|
||||
If there are issues with the signature, or if TLS trust fails, further debugging can
|
||||
be performed with OpenSSL on the command line.
|
||||
|
||||
Export the signature and the signed text, from the [Rails console](../../../../administration/operations/rails_console.md#starting-a-rails-console-session):
|
||||
|
||||
1. The initial two steps from [the main verification checks](#main-verification-checks) are required so `signature` has been set.
|
||||
|
||||
1. OpenSSL requires that PKCS7 PEM formatted data is bounded with `BEGIN PKCS7` and `END PKCS7` so this usually needs to be fixed:
|
||||
|
||||
```ruby
|
||||
pkcs7_text = signature.signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----')
|
||||
pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----')
|
||||
```
|
||||
|
||||
1. Write out the signature and signed text:
|
||||
|
||||
```ruby
|
||||
f1=File.new('/tmp/signature_text.pk7.pem','w')
|
||||
f1 << pkcs7_text
|
||||
f1.close
|
||||
|
||||
f2=File.new('/tmp/signed_text.txt','w')
|
||||
f2 << signature.signed_text
|
||||
f2.close
|
||||
```
|
||||
|
||||
This data can now be investigated on the Linux command line using OpenSSL:
|
||||
|
||||
1. The PKCS #7 file containing the signature can be queried:
|
||||
|
||||
```shell
|
||||
/opt/gitlab/embedded/bin/openssl pkcs7 -inform pem -print_certs \
|
||||
-in /tmp/signature_text.pk7.pem -print -noout
|
||||
```
|
||||
|
||||
It should include at least one `cert` section in the output; the signer's certificate.
|
||||
|
||||
There's a lot of low level of detail in the output. Here's an example of some of the structure and headings that should be present:
|
||||
|
||||
```plaintext
|
||||
PKCS7:
|
||||
d.sign:
|
||||
cert:
|
||||
cert_info:
|
||||
issuer:
|
||||
validity:
|
||||
notBefore:
|
||||
notAfter:
|
||||
subject:
|
||||
```
|
||||
|
||||
If developers' code signing certificates are issued by an intermediate certificate authority,
|
||||
there should be additional certificate details:
|
||||
|
||||
```plaintext
|
||||
PKCS7:
|
||||
d.sign:
|
||||
cert:
|
||||
cert_info:
|
||||
cert:
|
||||
cert_info:
|
||||
```
|
||||
|
||||
1. Extract the certificate from the signature:
|
||||
|
||||
```shell
|
||||
/opt/gitlab/embedded/bin/openssl pkcs7 -inform pem -print_certs \
|
||||
-in /tmp/signature_text.pk7.pem -out /tmp/signature_cert.pem
|
||||
```
|
||||
|
||||
If this step fails, the signature might be missing the signer's certificate.
|
||||
|
||||
- Fix this issue on the Git client.
|
||||
- The following step will fail, but if you copy the signer's certificate to the
|
||||
GitLab server, you can use that to do some testing using `-nointern -certfile signerscertificate.pem`.
|
||||
|
||||
1. Partially verify the commit, using the extracted certificate:
|
||||
|
||||
```shell
|
||||
/opt/gitlab/embedded/bin/openssl smime -verify -binary -inform pem \
|
||||
-in /tmp/signature_text.pk7.pem -content /tmp/signed_text.txt \
|
||||
-noverify -certfile /tmp/signature_cert.pem -nointern
|
||||
```
|
||||
|
||||
The output usually includes:
|
||||
|
||||
- The parent commit
|
||||
- The name, email, and timestamp from the commit
|
||||
- The commit text
|
||||
- `Verification successful` (or similar)
|
||||
|
||||
This check is not the same as the check GitLab performs, because:
|
||||
|
||||
- It does not verify the signer's certificate (`-noverify`)
|
||||
- The verification is done using the supplied `-certfile` rather than the one in the message (`-nointern`)
|
||||
|
||||
1. Partially verify the commit using the certificate in the message:
|
||||
|
||||
```shell
|
||||
/opt/gitlab/embedded/bin/openssl smime -verify -binary -inform pem \
|
||||
-in /tmp/signature_text.pk7.pem -content /tmp/signed_text.txt \
|
||||
-noverify
|
||||
```
|
||||
|
||||
This should get the same result as the previous step, using the extracted certificate.
|
||||
|
||||
If the message is missing the certificate, the error will include `signer certificate not found`.
|
||||
|
||||
1. Fully verify the commit:
|
||||
|
||||
```shell
|
||||
/opt/gitlab/embedded/bin/openssl smime -verify -binary -inform pem \
|
||||
-in /tmp/signature_text.pk7.pem -content /tmp/signed_text.txt
|
||||
```
|
||||
|
||||
If this step fails, verification will also fail in GitLab.
|
||||
|
||||
Resolve any errors, for example:
|
||||
|
||||
- `certificate verify error .. unable to get local issuer certificate`:
|
||||
- The trust chain couldn't be established.
|
||||
- This OpenSSL binary uses the GitLab trust store. Either the root certificate is missing from the trust store
|
||||
or the signature is missing the intermediate certificate(s) and a chain to a trusted root can't be built.
|
||||
- Intermediate certificates can be put in the trust store if it's not possible to include them in the signature.
|
||||
- [The procedure for adding certificates](https://docs.gitlab.com/omnibus/settings/ssl/#install-custom-public-certificates)
|
||||
to the trust store for packaged GitLab - using `/etc/gitlab/trusted-certs`.
|
||||
- Test additional trusted certificates using OpenSSL with: `-CAfile /path/to/rootcertificate.pem`
|
||||
- `unsupported certificate purpose`:
|
||||
- The certificate must specify `Digital Signature` under `Key Usage`.
|
||||
- This is usually in the `X509v3 Key Usage` section of the signer's certificate.
|
||||
- There is also a `X509v3 Extended Key Usage` section: if this is specified, it must include `Digital Signature` as well.
|
||||
See [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#page-44) for more details:
|
||||
|
||||
> If there is no purpose consistent with both (Key Usage) extensions, then the certificate MUST NOT be used for any purpose.
|
||||
|
||||
- `signer certificate not found`, either:
|
||||
- You have added the `-nointern` argument, but not supplied `-certfile`.
|
||||
- The signature is missing the signer's certificate.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ code_quality:
|
|||
DOCKER_SOCKET_PATH: /var/run/docker.sock
|
||||
needs: []
|
||||
script:
|
||||
- export SOURCE_CODE=$PWD
|
||||
- export SOURCE_CODE=${SOURCE_CODE:-$PWD}
|
||||
- |
|
||||
if ! docker info &>/dev/null; then
|
||||
if [ -z "$DOCKER_HOST" ] && [ -n "$KUBERNETES_PORT" ]; then
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
ALL_CLASSES = [
|
||||
Gitlab::Redis::BufferedCounter,
|
||||
Gitlab::Redis::Cache,
|
||||
Gitlab::Redis::ClusterRepositoryCache,
|
||||
Gitlab::Redis::DbLoadBalancing,
|
||||
Gitlab::Redis::FeatureFlag,
|
||||
Gitlab::Redis::Queues,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Redis
|
||||
class ClusterRepositoryCache < ::Gitlab::Redis::Wrapper
|
||||
class << self
|
||||
# The data we store on RepositoryCache used to be stored on Cache.
|
||||
def config_fallback
|
||||
Cache
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -144,7 +144,7 @@ module Gitlab
|
|||
@primary_pool = primary_pool
|
||||
@secondary_pool = secondary_pool
|
||||
|
||||
@borrow_counter = "multi_store_borrowed_connection_#{instance_name}".to_sym
|
||||
@borrow_counter = :"multi_store_borrowed_connection_#{instance_name}"
|
||||
|
||||
validate_stores!
|
||||
end
|
||||
|
|
@ -252,18 +252,24 @@ module Gitlab
|
|||
end
|
||||
|
||||
def use_primary_and_secondary_stores?
|
||||
feature_flag = "use_primary_and_secondary_stores_for_#{instance_name.underscore}"
|
||||
|
||||
# We interpolate the feature flag name within `Feature.enabled?` instead of defining a variable to allow
|
||||
# `RuboCop::Cop::Gitlab::MarkUsedFeatureFlags`'s optimistic matching to work.
|
||||
feature_table_exists? &&
|
||||
Feature.enabled?(feature_flag, type: feature_flag_type(feature_flag)) && # rubocop:disable Cop/FeatureFlagUsage -- The flags are dynamic
|
||||
Feature.enabled?( # rubocop:disable Cop/FeatureFlagUsage -- The flags are dynamic
|
||||
"use_primary_and_secondary_stores_for_#{instance_name.underscore}",
|
||||
type: feature_flag_type("use_primary_and_secondary_stores_for_#{instance_name.underscore}")
|
||||
) &&
|
||||
!same_redis_store?
|
||||
end
|
||||
|
||||
def use_primary_store_as_default?
|
||||
feature_flag = "use_primary_store_as_default_for_#{instance_name.underscore}"
|
||||
|
||||
# We interpolate the feature flag name within `Feature.enabled?` instead of defining a variable to allow
|
||||
# `RuboCop::Cop::Gitlab::MarkUsedFeatureFlags`'s optimistic matching to work.
|
||||
feature_table_exists? &&
|
||||
Feature.enabled?(feature_flag, type: feature_flag_type(feature_flag)) && # rubocop:disable Cop/FeatureFlagUsage -- The flags are dynamic
|
||||
Feature.enabled?( # rubocop:disable Cop/FeatureFlagUsage -- The flags are dynamic
|
||||
"use_primary_store_as_default_for_#{instance_name.underscore}",
|
||||
type: feature_flag_type("use_primary_store_as_default_for_#{instance_name.underscore}")
|
||||
) &&
|
||||
!same_redis_store?
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Gitlab
|
||||
module Redis
|
||||
class RepositoryCache < ::Gitlab::Redis::Wrapper
|
||||
class RepositoryCache < ::Gitlab::Redis::MultiStoreWrapper
|
||||
# We create a subclass only for the purpose of differentiating between different stores in cache metrics
|
||||
RepositoryCacheStore = Class.new(ActiveSupport::Cache::RedisCacheStore)
|
||||
|
||||
|
|
@ -21,6 +21,10 @@ module Gitlab
|
|||
expires_in: Cache.default_ttl_seconds
|
||||
)
|
||||
end
|
||||
|
||||
def multistore
|
||||
MultiStore.new(ClusterRepositoryCache.pool, pool, store_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29701,18 +29701,6 @@ msgstr ""
|
|||
msgid "MLExperimentTracking|Delete experiment?"
|
||||
msgstr ""
|
||||
|
||||
msgid "MR widget|Back to the merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "MR widget|See your pipeline in action"
|
||||
msgstr ""
|
||||
|
||||
msgid "MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more."
|
||||
msgstr ""
|
||||
|
||||
msgid "MR widget|The pipeline will test your code on every commit. A %{codeQualityLinkStart}code quality report%{codeQualityLinkEnd} will appear in your merge requests to warn you about potential code degradations."
|
||||
msgstr ""
|
||||
|
||||
msgid "MRApprovals|Approvals"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -49725,9 +49713,6 @@ msgstr ""
|
|||
msgid "That's OK, I don't want to renew"
|
||||
msgstr ""
|
||||
|
||||
msgid "That's it, well done!"
|
||||
msgstr ""
|
||||
|
||||
msgid "The %{plan_name} is no longer available to purchase. For more information about how this will impact you, check our %{faq_link_start}frequently asked questions%{faq_link_end}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60268,21 +60253,6 @@ msgstr ""
|
|||
msgid "success"
|
||||
msgstr ""
|
||||
|
||||
msgid "suggestPipeline|1/2: Choose a template"
|
||||
msgstr ""
|
||||
|
||||
msgid "suggestPipeline|2/2: Commit your changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "suggestPipeline|Choose %{boldStart}Code Quality%{boldEnd} to add a pipeline that tests the quality of your code."
|
||||
msgstr ""
|
||||
|
||||
msgid "suggestPipeline|The template is ready! You can now commit it to create your first pipeline."
|
||||
msgstr ""
|
||||
|
||||
msgid "suggestPipeline|We’re adding a GitLab CI configuration file to add a pipeline to the project. You could create it manually, but we recommend that you start with a GitLab template that works out of the box."
|
||||
msgstr ""
|
||||
|
||||
msgid "supported SSH public key."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
# frozen_string_literal: true
|
||||
|
||||
ENV['RAILS_ENV'] = 'test'
|
||||
|
||||
require 'optparse'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
|
@ -49,6 +51,7 @@ class SchemaRegenerator
|
|||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135085#note_1628210334 for more info.
|
||||
#
|
||||
load_tasks
|
||||
drop_db
|
||||
checkout_ref
|
||||
checkout_clean_schema
|
||||
|
|
@ -67,6 +70,11 @@ class SchemaRegenerator
|
|||
|
||||
private
|
||||
|
||||
def load_tasks
|
||||
require_relative '../config/environment'
|
||||
Gitlab::Application.load_tasks
|
||||
end
|
||||
|
||||
##
|
||||
# Git checkout +CI_COMMIT_SHA+.
|
||||
#
|
||||
|
|
@ -181,25 +189,25 @@ class SchemaRegenerator
|
|||
##
|
||||
# Run rake task to drop the database.
|
||||
def drop_db
|
||||
run %q(bin/rails db:drop RAILS_ENV=test)
|
||||
run_rake_task 'db:drop'
|
||||
end
|
||||
|
||||
##
|
||||
# Run rake task to setup the database.
|
||||
def setup_db
|
||||
run %q(bin/rails db:setup RAILS_ENV=test)
|
||||
run_rake_task 'db:setup'
|
||||
end
|
||||
|
||||
##
|
||||
# Run rake task to run migrations.
|
||||
def migrate
|
||||
run %q(bin/rails db:migrate RAILS_ENV=test)
|
||||
run_rake_task 'db:migrate'
|
||||
end
|
||||
|
||||
##
|
||||
# Run rake task to dump schema.
|
||||
def dump_schema
|
||||
run %q(bin/rails db:schema:dump RAILS_ENV=test)
|
||||
run_rake_task 'db:schema:dump'
|
||||
end
|
||||
|
||||
##
|
||||
|
|
@ -226,6 +234,15 @@ class SchemaRegenerator
|
|||
stdout_str
|
||||
end
|
||||
|
||||
def run_rake_task(*tasks, env: {})
|
||||
Array.wrap(tasks).each do |task|
|
||||
env.each { |k, v| ENV[k.to_s] = v.to_s }
|
||||
|
||||
puts "\e[32m$ bin/rails #{task} RAILS_ENV=test #{env.map { |m| m.join('=') }.join(' ')}\e[37m"
|
||||
Rake::Task[task].invoke
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Return the base commit between source and target branch.
|
||||
def merge_base
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled', :js, feature_category: :source_code_management do
|
||||
include CookieHelper
|
||||
|
||||
let(:project) { create(:project, :empty_repo) }
|
||||
let(:user) { project.first_owner }
|
||||
|
||||
describe 'viewing the new blob page' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when the page is loaded from the link using the suggest_gitlab_ci_yml param' do
|
||||
before do
|
||||
visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master', suggest_gitlab_ci_yml: 'true')
|
||||
end
|
||||
|
||||
it 'pre-fills .gitlab-ci.yml for file name' do
|
||||
file_name = page.find_by_id('file_name')
|
||||
|
||||
expect(file_name.value).to have_content('.gitlab-ci.yml')
|
||||
end
|
||||
|
||||
it 'displays suggest_gitlab_ci_yml popover' do
|
||||
popover_selector = '.suggest-gitlab-ci-yml'
|
||||
|
||||
expect(page).to have_css(popover_selector, visible: true)
|
||||
|
||||
page.within(popover_selector) do
|
||||
expect(page).to have_content('1/2: Choose a template')
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the commit cookie when the Commit button is clicked' do
|
||||
click_button 'Commit changes'
|
||||
|
||||
expect(get_cookie("suggest_gitlab_ci_yml_commit_#{project.id}")).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the page is visited without the param' do
|
||||
before do
|
||||
visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master')
|
||||
end
|
||||
|
||||
it 'does not pre-fill .gitlab-ci.yml for file name' do
|
||||
file_name = page.find_by_id('file_name')
|
||||
|
||||
expect(file_name.value).not_to have_content('.gitlab-ci.yml')
|
||||
end
|
||||
|
||||
it 'does not display suggest_gitlab_ci_yml popover' do
|
||||
popover_selector = '.b-popover.suggest-gitlab-ci-yml'
|
||||
|
||||
expect(page).not_to have_css(popover_selector, visible: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
export const SuggestCiYmlData = {
|
||||
trackLabel: 'suggest_gitlab_ci_yml',
|
||||
dismissKey: '10',
|
||||
mergeRequestPath: 'mr_path',
|
||||
humanAccess: 'owner',
|
||||
};
|
||||
|
||||
export const Templates = {
|
||||
licenses: {
|
||||
Other: [
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import TemplateSelector from '~/blob/filepath_form/components/template_selector.vue';
|
||||
import SuggestGitlabCiYml from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
|
||||
import { Templates as TemplatesMock, SuggestCiYmlData as SuggestCiYmlDataMock } from './mock_data';
|
||||
import { Templates as TemplatesMock } from './mock_data';
|
||||
|
||||
describe('Template Selector component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findSuggestCiYmlPopover = () => wrapper.findComponent(SuggestGitlabCiYml);
|
||||
const findDisplayedTemplates = () =>
|
||||
findListbox()
|
||||
.props('items')
|
||||
|
|
@ -39,10 +37,6 @@ describe('Template Selector component', () => {
|
|||
it('does not render listbox', () => {
|
||||
expect(findListbox().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render suggest-ci-yml popover', () => {
|
||||
expect(findSuggestCiYmlPopover().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
|
@ -62,26 +56,6 @@ describe('Template Selector component', () => {
|
|||
expect(findListbox().props('searchPlaceholder')).toBe('Filter');
|
||||
expect(findDisplayedTemplates()).toEqual(getTemplateKeysFromMock(key));
|
||||
});
|
||||
|
||||
it('does not render suggest-ci-yml popover', () => {
|
||||
expect(findSuggestCiYmlPopover().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when filename input is .gitlab-ci.yml with suggestCiYmlData prop', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ filename: '.gitlab-ci.yml', suggestCiYmlData: SuggestCiYmlDataMock });
|
||||
});
|
||||
|
||||
it('renders listbox with correct props', () => {
|
||||
expect(findListbox().exists()).toBe(true);
|
||||
expect(findListbox().props('toggleText')).toBe('Apply a template');
|
||||
expect(findListbox().props('searchPlaceholder')).toBe('Filter');
|
||||
});
|
||||
|
||||
it('renders suggest-ci-yml popover', () => {
|
||||
expect(findSuggestCiYmlPopover().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has filename that matches template pattern', () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
const modalProps = {
|
||||
goToPipelinesPath: 'some_pipeline_path',
|
||||
projectMergeRequestsPath: 'some_mr_path',
|
||||
commitCookie: 'some_cookie',
|
||||
humanAccess: 'maintainer',
|
||||
exampleLink: '/example',
|
||||
codeQualityLink: '/code-quality-link',
|
||||
};
|
||||
|
||||
export default modalProps;
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import { GlSprintf, GlModal, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Cookies from '~/lib/utils/cookies';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
|
||||
import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
|
||||
import modalProps from './pipeline_tour_success_mock_data';
|
||||
|
||||
describe('PipelineTourSuccessModal', () => {
|
||||
let wrapper;
|
||||
let cookieSpy;
|
||||
let trackingSpy;
|
||||
|
||||
const GlEmoji = { template: '<img/>' };
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(pipelineTourSuccess, {
|
||||
propsData: modalProps,
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal, {
|
||||
template: `
|
||||
<div>
|
||||
<slot name="modal-title"></slot>
|
||||
<slot></slot>
|
||||
<slot name="modal-footer"></slot>
|
||||
</div>`,
|
||||
}),
|
||||
GlSprintf,
|
||||
GlEmoji,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.dataset.page = 'projects:blob:show';
|
||||
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
|
||||
cookieSpy = jest.spyOn(Cookies, 'remove');
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
Cookies.remove(modalProps.commitCookie);
|
||||
});
|
||||
|
||||
describe('when the commitCookie contains the mr path', () => {
|
||||
const expectedMrPath = 'expected_mr_path';
|
||||
|
||||
beforeEach(() => {
|
||||
Cookies.set(modalProps.commitCookie, expectedMrPath);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the path from the commit cookie for back to the merge request button', () => {
|
||||
const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
|
||||
|
||||
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the commitCookie does not contain mr path', () => {
|
||||
const expectedMrPath = modalProps.projectMergeRequestsPath;
|
||||
|
||||
beforeEach(() => {
|
||||
Cookies.set(modalProps.commitCookie, true);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the path from projectMergeRequestsPath for back to the merge request button', () => {
|
||||
const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
|
||||
|
||||
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
|
||||
});
|
||||
});
|
||||
|
||||
it('has expected structure', () => {
|
||||
const modal = wrapper.findComponent(GlModal);
|
||||
const sprintf = modal.findComponent(GlSprintf);
|
||||
const emoji = modal.findComponent(GlEmoji);
|
||||
|
||||
expect(wrapper.text()).toContain("That's it, well done!");
|
||||
expect(sprintf.exists()).toBe(true);
|
||||
expect(emoji.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the link for codeQualityLink', () => {
|
||||
expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/code-quality-link');
|
||||
});
|
||||
|
||||
it('calls to remove cookie', () => {
|
||||
wrapper.vm.disableModalFromRenderingAgain();
|
||||
|
||||
expect(cookieSpy).toHaveBeenCalledWith(modalProps.commitCookie);
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
it('send event for basic view of modal', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, undefined, {
|
||||
label: 'congratulate_first_pipeline',
|
||||
property: modalProps.humanAccess,
|
||||
});
|
||||
});
|
||||
|
||||
it('send an event when go to pipelines is clicked', () => {
|
||||
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
|
||||
const goToBtn = wrapper.findComponent({ ref: 'goToPipelines' });
|
||||
triggerEvent(goToBtn.element);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
|
||||
label: 'congratulate_first_pipeline',
|
||||
property: modalProps.humanAccess,
|
||||
value: '10',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends an event when back to the merge request is clicked', () => {
|
||||
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
|
||||
const goToBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
|
||||
triggerEvent(goToBtn.element);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
|
||||
label: 'congratulate_first_pipeline',
|
||||
property: modalProps.humanAccess,
|
||||
value: '20',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
|
||||
import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
|
||||
import * as utils from '~/lib/utils/common_utils';
|
||||
|
||||
jest.mock('~/lib/utils/common_utils', () => ({
|
||||
...jest.requireActual('~/lib/utils/common_utils'),
|
||||
scrollToElement: jest.fn(),
|
||||
}));
|
||||
|
||||
const target = 'gitlab-ci-yml-selector';
|
||||
const dismissKey = '99';
|
||||
const defaultTrackLabel = 'suggest_gitlab_ci_yml';
|
||||
const commitTrackLabel = 'suggest_commit_first_project_gitlab_ci_yml';
|
||||
|
||||
const dismissCookie = 'suggest_gitlab_ci_yml_99';
|
||||
const humanAccess = 'owner';
|
||||
const mergeRequestPath = '/some/path';
|
||||
|
||||
describe('Suggest gitlab-ci.yml Popover', () => {
|
||||
let wrapper;
|
||||
|
||||
function createWrapper(trackLabel) {
|
||||
wrapper = shallowMount(Popover, {
|
||||
propsData: {
|
||||
target,
|
||||
trackLabel,
|
||||
dismissKey,
|
||||
mergeRequestPath,
|
||||
humanAccess,
|
||||
},
|
||||
stubs: {
|
||||
'gl-popover': { template: '<div><slot name="title"></slot><slot></slot></div>' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('when no dismiss cookie is set', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(defaultTrackLabel);
|
||||
});
|
||||
|
||||
it('sets popoverDismissed to false', () => {
|
||||
expect(wrapper.vm.popoverDismissed).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the dismiss cookie is set', () => {
|
||||
beforeEach(() => {
|
||||
utils.setCookie(dismissCookie, true);
|
||||
|
||||
createWrapper(defaultTrackLabel);
|
||||
});
|
||||
|
||||
it('sets popoverDismissed to true', () => {
|
||||
expect(wrapper.vm.popoverDismissed).toEqual(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
utils.removeCookie(dismissCookie);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.dataset.page = 'projects:blob:new';
|
||||
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
|
||||
|
||||
createWrapper(commitTrackLabel);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
it('sends a tracking event with the expected properties for the popover being viewed', () => {
|
||||
const expectedCategory = undefined;
|
||||
const expectedAction = undefined;
|
||||
const expectedLabel = 'suggest_commit_first_project_gitlab_ci_yml';
|
||||
const expectedProperty = 'owner';
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(expectedCategory, expectedAction, {
|
||||
label: expectedLabel,
|
||||
property: expectedProperty,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a tracking event when the popover is dismissed', () => {
|
||||
const expectedLabel = commitTrackLabel;
|
||||
const expectedAction = 'click_button';
|
||||
const expectedProperty = 'owner';
|
||||
const expectedValue = '10';
|
||||
const dismissButton = wrapper.findComponent(GlButton);
|
||||
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
|
||||
|
||||
triggerEvent(dismissButton.element);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', expectedAction, {
|
||||
label: expectedLabel,
|
||||
property: expectedProperty,
|
||||
value: expectedValue,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the popover is mounted with the trackLabel of the Confirm button popover at the bottom of the page', () => {
|
||||
it('calls scrollToElement so that the Confirm button and popover will be in sight', () => {
|
||||
const scrollToElementSpy = jest.spyOn(utils, 'scrollToElement');
|
||||
|
||||
createWrapper(commitTrackLabel);
|
||||
|
||||
expect(scrollToElementSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import blobBundle from '~/blob_edit/blob_bundle';
|
||||
|
||||
|
|
@ -64,46 +63,6 @@ describe('BlobBundle', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Suggest Popover', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(`
|
||||
<div class="js-edit-blob-form" data-blob-filename="blah" id="target">
|
||||
<div class="js-suggest-gitlab-ci-yml"
|
||||
data-target="#target"
|
||||
data-track-label="suggest_gitlab_ci_yml"
|
||||
data-dismiss-key="1"
|
||||
data-human-access="owner"
|
||||
data-merge-request-path="path/to/mr">
|
||||
<button id='commit-changes' class="js-commit-button"></button>
|
||||
<button id='cancel-changes'></button>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
trackingSpy = mockTracking('_category_', $('#commit-changes').element, jest.spyOn);
|
||||
document.body.dataset.page = 'projects:blob:new';
|
||||
|
||||
blobBundle();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('sends a tracking event when the commit button is clicked', () => {
|
||||
$('#commit-changes').click();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'suggest_gitlab_ci_yml_commit_changes',
|
||||
property: 'owner',
|
||||
value: '20',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
let message;
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -207,83 +207,6 @@ RSpec.describe BlobHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show_suggest_pipeline_creation_celebration?' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
assign(:blob, blob)
|
||||
assign(:commit, double('Commit', sha: 'whatever'))
|
||||
helper.request.cookies["suggest_gitlab_ci_yml_commit_#{project.id}"] = 'true'
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
end
|
||||
|
||||
context 'when file is a pipeline config file' do
|
||||
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
|
||||
let(:blob) { fake_blob(path: project.ci_config_path_or_default, data: data) }
|
||||
|
||||
it 'is true' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
|
||||
end
|
||||
|
||||
context 'file is invalid format' do
|
||||
let(:data) { 'foo' }
|
||||
|
||||
it 'is false' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'does not use the default ci config' do
|
||||
before do
|
||||
project.ci_config_path = 'something_bad'
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'does not have the needed cookie' do
|
||||
before do
|
||||
helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}"
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'blob does not have auxiliary view' do
|
||||
before do
|
||||
allow(blob).to receive(:auxiliary_viewer).and_return(nil)
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is not a pipeline config file' do
|
||||
let(:blob) { fake_blob(path: 'LICENSE') }
|
||||
|
||||
it 'is false' do
|
||||
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'suggest_pipeline_commit_cookie_name' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'uses project id to make up the cookie name' do
|
||||
assign(:project, project)
|
||||
|
||||
expect(helper.suggest_pipeline_commit_cookie_name).to eq "suggest_gitlab_ci_yml_commit_#{project.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ide_edit_path' do
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe TabHelper do
|
|||
|
||||
describe 'gl_tabs_nav' do
|
||||
it 'creates a tabs navigation' do
|
||||
expect(helper.gl_tabs_nav).to match(%r{<ul class="nav gl-tabs-nav"></ul>})
|
||||
expect(helper.gl_tabs_nav).to match(%r{<ul role="tablist" class="nav gl-tabs-nav"></ul>})
|
||||
end
|
||||
|
||||
it 'captures block output' do
|
||||
|
|
@ -25,7 +25,7 @@ RSpec.describe TabHelper do
|
|||
end
|
||||
|
||||
it 'creates a tab' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url')).to eq('<li class="nav-item"><a class="nav-link gl-tab-nav-item" href="/url">Link</a></li>')
|
||||
expect(helper.gl_tab_link_to('Link', '/url')).to eq('<li role="presentation" class="nav-item"><a role="tab" class="nav-link gl-tab-nav-item" href="/url">Link</a></li>')
|
||||
end
|
||||
|
||||
it 'creates a tab with block output' do
|
||||
|
|
@ -33,19 +33,19 @@ RSpec.describe TabHelper do
|
|||
end
|
||||
|
||||
it 'creates a tab with custom classes for enclosing list item without content block provided' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { tab_class: 'my-class' })).to match(/<li class=".*my-class.*"/)
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { tab_class: 'my-class' })).to match(/<li role="presentation" class=".*my-class.*"/)
|
||||
end
|
||||
|
||||
it 'creates a tab with custom classes for enclosing list item with content block provided' do
|
||||
expect(helper.gl_tab_link_to('/url', { tab_class: 'my-class' }) { 'Link' }).to match(/<li class=".*my-class.*"/)
|
||||
expect(helper.gl_tab_link_to('/url', { tab_class: 'my-class' }) { 'Link' }).to match(/<li role="presentation" class=".*my-class.*"/)
|
||||
end
|
||||
|
||||
it 'creates a tab with custom classes for anchor element' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { class: 'my-class' })).to match(/<a class=".*my-class.*"/)
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { class: 'my-class' })).to match(/<a class=".*my-class.*" role="tab"/)
|
||||
end
|
||||
|
||||
it 'creates an active tab with item_active = true' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { item_active: true })).to match(/<a class=".*active gl-tab-nav-item-active.*"/)
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { item_active: true })).to match(/<a role="tab" class=".*active gl-tab-nav-item-active.*"/)
|
||||
end
|
||||
|
||||
context 'when on the active page' do
|
||||
|
|
@ -54,11 +54,11 @@ RSpec.describe TabHelper do
|
|||
end
|
||||
|
||||
it 'creates an active tab' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url')).to match(/<a class=".*active gl-tab-nav-item-active.*"/)
|
||||
expect(helper.gl_tab_link_to('Link', '/url')).to match(/<a role="tab" class=".*active gl-tab-nav-item-active.*"/)
|
||||
end
|
||||
|
||||
it 'creates an inactive tab with item_active = false' do
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { item_active: false })).not_to match(/<a class=".*active.*"/)
|
||||
expect(helper.gl_tab_link_to('Link', '/url', { item_active: false })).not_to match(/<a role="tab" class=".*active.*"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Redis::ClusterRepositoryCache, feature_category: :scalability do
|
||||
include_examples "redis_new_instance_shared_examples", 'cluster_repository_cache', Gitlab::Redis::Cache
|
||||
end
|
||||
|
|
@ -4,10 +4,16 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
|
||||
include_examples "redis_new_instance_shared_examples", 'repository_cache', Gitlab::Redis::Cache
|
||||
include_examples "multi_store_wrapper_shared_examples"
|
||||
|
||||
describe '.cache_store' do
|
||||
it 'has a default ttl of 8 hours' do
|
||||
expect(described_class.cache_store.options[:expires_in]).to eq(8.hours)
|
||||
end
|
||||
end
|
||||
|
||||
it 'migrates from self to ClusterRepositoryCache' do
|
||||
expect(described_class.multistore.secondary_pool).to eq(described_class.pool)
|
||||
expect(described_class.multistore.primary_pool).to eq(Gitlab::Redis::ClusterRepositoryCache.pool)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,31 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeline_composition do
|
||||
describe '#execute' do
|
||||
context 'when executing release service' do
|
||||
let(:histogram) { instance_double(Prometheus::Client::Histogram) }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Metrics).to receive(:histogram).and_call_original
|
||||
|
||||
allow(::Gitlab::Metrics).to receive(:histogram).with(
|
||||
:gitlab_ci_catalog_release_duration_seconds,
|
||||
'CI Catalog Release duration',
|
||||
{},
|
||||
[0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 240.0]
|
||||
).and_return(histogram)
|
||||
allow(::Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
|
||||
end
|
||||
|
||||
it 'tracks release duration' do
|
||||
project = create(:project, :catalog_resource_with_components)
|
||||
release = create(:release, project: project, sha: project.repository.root_ref_sha)
|
||||
|
||||
expect(histogram).to receive(:observe).with({}, an_instance_of(Float))
|
||||
|
||||
described_class.new(release).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid catalog resource and release' do
|
||||
it 'validates the catalog resource and creates a version' do
|
||||
project = create(:project, :catalog_resource_with_components)
|
||||
|
|
|
|||
|
|
@ -3733,7 +3733,6 @@
|
|||
- './spec/features/projects/blobs/blob_show_spec.rb'
|
||||
- './spec/features/projects/blobs/edit_spec.rb'
|
||||
- './spec/features/projects/blobs/shortcuts_blob_spec.rb'
|
||||
- './spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb'
|
||||
- './spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb'
|
||||
- './spec/features/projects/branches/download_buttons_spec.rb'
|
||||
- './spec/features/projects/branches/new_branch_ref_dropdown_spec.rb'
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ RSpec.describe 'admin/sessions/new.html.haml' do
|
|||
render
|
||||
|
||||
expect(rendered).to have_selector('[data-testid="ldap-tab"]')
|
||||
expect(rendered).to have_css('.login-box#ldapmain')
|
||||
expect(rendered).to have_css('#ldapmain')
|
||||
expect(rendered).to have_field(_('Username'))
|
||||
expect(rendered).not_to have_content('No authentication methods configured')
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue