diff options
65 files changed, 516 insertions, 95 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 48294e00844..1e6df6f5a77 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -42,6 +42,7 @@ rules: no-jquery/no-serialize: error promise/always-return: off promise/no-callback-in-promise: off + "@gitlab/no-global-event-off": error overrides: - files: - '**/spec/**/*' diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 5f50fcc112e..0a05e0d44ce 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -74,6 +74,7 @@ export default class Autosave { } dispose() { + // eslint-disable-next-line @gitlab/no-global-event-off this.field.off('input'); } } diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 17e6255700a..d937060536a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -596,6 +596,7 @@ export class AwardsHandler { hideMenuElement($emojiMenu) { $emojiMenu.on(transitionEndEventString, e => { if (e.currentTarget === e.target) { + // eslint-disable-next-line @gitlab/no-global-event-off $emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString); } }); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index a53150f8d61..c0f67923191 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -97,6 +97,7 @@ export default class Shortcuts { e.preventDefault(); }); + // eslint-disable-next-line @gitlab/no-global-event-off $('.js-shortcuts-modal-trigger') .off('click') .on('click', this.onToggleHelp); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 47eee5306da..d1011c24977 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -15,6 +15,7 @@ function shouldCreateListGraphQL(label) { return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label)); } +// eslint-disable-next-line @gitlab/no-global-event-off $(document) .off('created.label') .on('created.label', (e, label, addNewList) => { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index ba005e98d53..a533a1a78e8 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -265,13 +265,21 @@ export default class Clusters { removeListeners() { eventHub.$off('installApplication', this.installApplication); eventHub.$off('updateApplication', this.updateApplication); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('saveKnativeDomain'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('setKnativeDomain'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('setCrossplaneProviderStack'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('uninstallApplication'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('setIngressModSecurityEnabled'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('setIngressModSecurityMode'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('resetIngressModSecurityChanges'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('setFluentdSettings'); } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 28abe558f53..b70f8d6e736 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -72,12 +72,14 @@ export default class ImageFile { callback(e, left); }; + // eslint-disable-next-line @gitlab/no-global-event-off $el .off('mousedown') .off('touchstart') .on('mousedown', dragStart) .on('touchstart', dragStart); + // eslint-disable-next-line @gitlab/no-global-event-off $body .off('mouseup') .off('mousemove') diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 7321e4d18cc..4f7bc829b0c 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -14,6 +14,7 @@ function openConfirmDangerModal($form, $modal, text) { $submit.disable(); $input.focus(); + // eslint-disable-next-line @gitlab/no-global-event-off $input.off('input').on('input', function handleInput() { const confirmText = rstrip($(this).val()); if (confirmText === confirmTextMatch) { @@ -23,6 +24,7 @@ function openConfirmDangerModal($form, $modal, text) { } }); + // eslint-disable-next-line @gitlab/no-global-event-off $('.js-confirm-danger-submit', $modal) .off('click') .on('click', () => { diff --git a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue index e9d484bdd94..1e3a19b9da1 100644 --- a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue +++ b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue @@ -154,6 +154,7 @@ export default { }); }, beforeDestroy() { + // eslint-disable-next-line @gitlab/no-global-event-off $(this.$refs.dropdown).off(); }, methods: { diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 9c0ed7f79d4..0d53efe8689 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -29,11 +29,17 @@ export default class CreateLabelDropdown { } cleanBinding() { + // eslint-disable-next-line @gitlab/no-global-event-off this.$colorSuggestions.off('click'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$newLabelField.off('keyup change'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$newColorField.off('keyup change'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$dropdownBack.off('click'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$cancelButton.off('click'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$newLabelCreateButton.off('click'); } diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 4cccabca28b..70ebe91a3b2 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -74,6 +74,7 @@ export default () => { const $dropdown = $('.js-ca-dropdown'); const $label = $dropdown.find('.dropdown-label'); + // eslint-disable-next-line @gitlab/no-global-event-off $dropdown .find('li a') .off('click') diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js index c17f2d2efe4..fe57dd2dc8f 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js @@ -622,6 +622,7 @@ export class GitLabDropdown { // eslint-disable-next-line class-methods-use-this removeArrowKeyEvent() { + // eslint-disable-next-line @gitlab/no-global-event-off return $('body').off('keydown'); } diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index c1b9ba755a6..ce84caeb28f 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -93,7 +93,9 @@ export default { }, beforeDestroy() { + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('toggleFolder'); + // eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('toggleDeployBoard'); }, diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 14538ad7237..dcb27434a07 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -78,6 +78,7 @@ class GfmAutoComplete { this.input.each((i, input) => { const $input = $(input); if (!$input.hasClass('js-gfm-input-initialized')) { + // eslint-disable-next-line @gitlab/no-global-event-off $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); // This triggers at.js again diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index ac4c8d28ee4..60f1b7f5aa4 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -80,6 +80,7 @@ export default class GlFieldError { // hidden when injected into DOM errorAnchor.after(this.fieldErrorElement); + // eslint-disable-next-line @gitlab/no-global-event-off this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); this.scopedSiblings = this.safelySelectSiblings(); } @@ -117,6 +118,7 @@ export default class GlFieldError { this.form.focusInvalid.apply(this.form); // For UX, wait til after first invalid submission to check each keyup + // eslint-disable-next-line @gitlab/no-global-event-off this.inputElement .off('keyup.fieldValidator') .on('keyup.fieldValidator', this.updateValidity.bind(this)); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 6958cf4c173..4a3755f39cc 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -70,8 +70,10 @@ export default class GLForm { } setupAutosize() { + // eslint-disable-next-line @gitlab/no-global-event-off this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this)); + // eslint-disable-next-line @gitlab/no-global-event-off this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this)); setTimeout(() => { @@ -97,7 +99,9 @@ export default class GLForm { } clearEventListeners() { + // eslint-disable-next-line @gitlab/no-global-event-off this.textarea.off('focus'); + // eslint-disable-next-line @gitlab/no-global-event-off this.textarea.off('blur'); removeMarkdownListeners(this.form); } diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue index a2338c6dec5..8cea8655461 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -30,6 +30,7 @@ export default { .on('hide.bs.dropdown', () => this.hideDropdown()); }, removeDropdownListeners() { + // eslint-disable-next-line @gitlab/no-global-event-off $(this.$refs.dropdown) .off('show.bs.dropdown') .off('hide.bs.dropdown'); diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index c7806fc17fc..6ba21cd7869 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -15,6 +15,7 @@ export default { }, bindEvents() { + // eslint-disable-next-line @gitlab/no-global-event-off return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); }, diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 8acda4ada16..61e5db0970a 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -3,7 +3,6 @@ import { GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import Visibility from 'visibilityjs'; import { __, s__, sprintf } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { sanitize } from '~/lib/dompurify'; import { visitUrl } from '~/lib/utils/url_utility'; import Poll from '~/lib/utils/poll'; import eventHub from '../event_hub'; @@ -179,7 +178,7 @@ export default { const store = new Store({ titleHtml: this.initialTitleHtml, titleText: this.initialTitleText, - descriptionHtml: sanitize(this.initialDescriptionHtml), + descriptionHtml: this.initialDescriptionHtml, descriptionText: this.initialDescriptionText, updatedAt: this.updatedAt, updatedByName: this.updatedByName, diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js index aacbb6a9c6f..12f38005366 100644 --- a/app/assets/javascripts/issue_show/utils/parse_data.js +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -4,13 +4,11 @@ import { sanitize } from '~/lib/dompurify'; // We currently load + parse the data from the issue app and related merge request let cachedParsedData; -export const parseIssuableData = () => { +export const parseIssuableData = el => { try { if (cachedParsedData) return cachedParsedData; - const initialDataEl = document.getElementById('js-issuable-app'); - - const parsedData = JSON.parse(initialDataEl.dataset.initial); + const parsedData = JSON.parse(el.dataset.initial); parsedData.initialTitleHtml = sanitize(parsedData.initialTitleHtml); parsedData.initialDescriptionHtml = sanitize(parsedData.initialDescriptionHtml); diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue index 0d4f5bce965..0ce2bcc1cce 100644 --- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue @@ -215,6 +215,7 @@ export default { this.fetchIssuables(); }, beforeDestroy() { + // eslint-disable-next-line @gitlab/no-global-event-off issueableEventHub.$off('issuables:toggleBulkEdit'); }, methods: { diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index dfb86787788..c711c0bd163 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -339,6 +339,7 @@ export function addMarkdownListeners(form) { Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn); }); + // eslint-disable-next-line @gitlab/no-global-event-off const $allToolbarBtns = $('.js-md', form) .off('click') .on('click', function() { @@ -351,6 +352,7 @@ export function addMarkdownListeners(form) { } export function addEditorMarkdownListeners(editor) { + // eslint-disable-next-line @gitlab/no-global-event-off $('.js-md') .off('click') .on('click', e => { @@ -376,5 +378,6 @@ export function removeMarkdownListeners(form) { Shortcuts.removeMarkdownEditorShortcuts($(this)); }); + // eslint-disable-next-line @gitlab/no-global-event-off return $('.js-md', form).off('click'); } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index f5ba933d012..de7648c31b1 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -77,6 +77,7 @@ if (process.env.NODE_ENV !== 'production' && gon?.test_env) { document.addEventListener('beforeunload', () => { // Unbind scroll events + // eslint-disable-next-line @gitlab/no-global-event-off $(document).off('scroll'); // Close any open tooltips tooltips.dispose(document.querySelectorAll('.has-tooltip, [data-toggle="tooltip"]')); diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 6dd4018f87a..5bd228496da 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -11,9 +11,11 @@ export default class Members { } addListeners() { + // eslint-disable-next-line @gitlab/no-global-event-off $('.js-member-update-control') .off('change') .on('change', this.formSubmit.bind(this)); + // eslint-disable-next-line @gitlab/no-global-event-off $('.js-edit-member-form') .off('ajax:success') .on('ajax:success', this.formSuccess.bind(this)); diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js index 818ca8aa847..18ea27e9a34 100644 --- a/app/assets/javascripts/mirrors/mirror_repos.js +++ b/app/assets/javascripts/mirrors/mirror_repos.js @@ -39,6 +39,7 @@ export default class MirrorRepos { initMirrorSSH() { if (this.$password) { + // eslint-disable-next-line @gitlab/no-global-event-off this.$password.off('input.updateUrl'); } this.$password = undefined; diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js index eecfaa76168..c6486350f3b 100644 --- a/app/assets/javascripts/mirrors/ssh_mirror.js +++ b/app/assets/javascripts/mirrors/ssh_mirror.js @@ -185,10 +185,15 @@ export default class SSHMirror { } destroy() { + // eslint-disable-next-line @gitlab/no-global-event-off this.$repositoryUrl.off('keyup'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$form.find('.js-known-hosts').off('keyup'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$dropdownAuthType.off('change'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$btnDetectHostKeys.off('click'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$btnSSHHostsShowAdvanced.off('click'); } } diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index bda2adeb62a..170c5ff7695 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -367,6 +367,7 @@ export default { }, ); + // eslint-disable-next-line @gitlab/no-global-event-off eChart.off('datazoom'); eChart.on('datazoom', this.throttledDatazoom); }, diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 37bb79defd1..9a887021e5d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -187,6 +187,7 @@ export default class Notes { this.$wrapperEl.off('click', '.js-discussion-reply-button'); this.$wrapperEl.off('click', '.js-add-diff-note-button'); this.$wrapperEl.off('click', '.js-add-image-diff-note-button'); + // eslint-disable-next-line @gitlab/no-global-event-off this.$wrapperEl.off('visibilitychange'); this.$wrapperEl.off('keyup input', '.js-note-text'); this.$wrapperEl.off('click', '.js-note-target-reopen'); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 2aa37842707..f9a91ec322b 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -72,6 +72,7 @@ export default { }, initLoadMore() { + // eslint-disable-next-line @gitlab/no-global-event-off $(document).off('scroll'); $(document).endlessScroll({ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 4b15e435f60..614f8262e5b 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -17,7 +17,8 @@ import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; import { IssuableType } from '~/issuable_show/constants'; export default function() { - const { issueType, ...issuableData } = parseIssuableData(); + const initialDataEl = document.getElementById('js-issuable-app'); + const { issueType, ...issuableData } = parseIssuableData(initialDataEl); switch (issueType) { case IssuableType.Incident: diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql new file mode 100644 index 00000000000..149cb256ced --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql @@ -0,0 +1,11 @@ +#import "~/pipelines/graphql/queries/pipeline_stages.fragment.graphql" + +query getCiConfigData($content: String!) { + ciConfig(content: $content) { + errors + status + stages { + ...PipelineStagesData + } + } +} diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index a4bdc27d1a0..b1c52ffa920 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -1,7 +1,7 @@ <script> import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; -import { redirectTo, mergeUrlParams, refreshCurrentPage } from '~/lib/utils/url_utility'; +import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import CommitForm from './components/commit/commit_form.vue'; @@ -9,24 +9,25 @@ import TextEditor from './components/text_editor.vue'; import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql'; import getBlobContent from './graphql/queries/blob_content.graphql'; +import getCiConfigData from './graphql/queries/ci_config.graphql'; const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; const MR_TARGET_BRANCH = 'merge_request[target_branch]'; -const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF'; -const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE'; -const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; const COMMIT_FAILURE = 'COMMIT_FAILURE'; const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; +const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE'; +const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF'; +const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; export default { components: { + CommitForm, GlAlert, GlLoadingIcon, GlTab, GlTabs, PipelineGraph, - CommitForm, TextEditor, }, props: { @@ -55,14 +56,15 @@ export default { }, data() { return { - showFailureAlert: false, + ciConfigData: {}, + content: '', + contentModel: '', + currentTabIndex: 0, + editorIsReady: false, failureType: null, failureReasons: [], - isSaving: false, - editorIsReady: false, - content: '', - contentModel: '', + showFailureAlert: false, }; }, apollo: { @@ -85,18 +87,35 @@ export default { this.handleBlobContentError(error); }, }, + ciConfigData: { + query: getCiConfigData, + // If content is not loaded, we can't lint the data + skip: ({ contentModel }) => { + return !contentModel; + }, + variables() { + return { + content: this.contentModel, + }; + }, + update(data) { + return data?.ciConfig ?? {}; + }, + error() { + this.reportFailure(LOAD_FAILURE_UNKNOWN); + }, + }, }, computed: { - isLoading() { + isBlobContentLoading() { return this.$apollo.queries.content.loading; }, + isVisualizeTabActive() { + return this.currentTabIndex === 1; + }, defaultCommitMessage() { return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath }); }, - pipelineData() { - // Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141 - return {}; - }, failure() { switch (this.failureType) { case LOAD_FAILURE_NO_REF: @@ -233,17 +252,17 @@ export default { </ul> </gl-alert> <div class="gl-mt-4"> - <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" /> + <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" /> <div v-else class="file-editor gl-mb-3"> - <gl-tabs> + <gl-tabs v-model="currentTabIndex"> <!-- editor should be mounted when its tab is visible, so the container has a size --> <gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady"> <!-- editor should be mounted only once, when the tab is displayed --> <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" /> </gl-tab> - <gl-tab :title="$options.i18n.tabGraph"> - <pipeline-graph :pipeline-data="pipelineData" /> + <gl-tab :title="$options.i18n.tabGraph" :lazy="!isVisualizeTabActive"> + <pipeline-graph :pipeline-data="ciConfigData" /> </gl-tab> </gl-tabs> </div> diff --git a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql b/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql new file mode 100644 index 00000000000..0aef2fdfd7f --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql @@ -0,0 +1,12 @@ +fragment PipelineStagesData on CiConfigStage { + name + groups { + name + jobs { + name + needs { + name + } + } + } +} diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 2f35c4485f9..0e12c219e45 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -55,6 +55,7 @@ export default class ProjectFindFile { } initEvent() { + // eslint-disable-next-line @gitlab/no-global-event-off this.inputElement.off('keyup'); this.inputElement.on('keyup', event => { const target = $(event.target); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index d74a2d06786..e0f8740e5b7 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -26,12 +26,14 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr }; const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { + // eslint-disable-next-line @gitlab/no-global-event-off $projectNameInput.off('keyup change').on('keyup change', () => { onProjectNameChange($projectNameInput, $projectPathInput); hasUserDefinedProjectName = $projectNameInput.val().trim().length > 0; hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; }); + // eslint-disable-next-line @gitlab/no-global-event-off $projectPathInput.off('keyup change').on('keyup change', () => { onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName); hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; @@ -137,6 +139,7 @@ const bindEvents = () => { target.focus(); }) .on('hide.bs.popover', () => { + // eslint-disable-next-line @gitlab/no-global-event-off $(document).off('click.popover touchstart.popover'); }); } diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue index 9809b228308..b05a873e939 100644 --- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue +++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -97,7 +97,9 @@ export default { }, beforeDestroy() { const $input = $(this.$refs.input); + // eslint-disable-next-line @gitlab/no-global-event-off $input.off('shown-issues.atwho'); + // eslint-disable-next-line @gitlab/no-global-event-off $input.off('hidden-issues.atwho'); $input.off('inserted-issues.atwho', this.onInput); }, diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 87c8aa541d8..6f43f837374 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -23,8 +23,11 @@ Sidebar.initialize = function() { Sidebar.prototype.removeListeners = function() { this.sidebar.off('click', '.sidebar-collapsed-icon'); + // eslint-disable-next-line @gitlab/no-global-event-off this.sidebar.off('hidden.gl.dropdown'); + // eslint-disable-next-line @gitlab/no-global-event-off $('.dropdown').off('loading.gl.dropdown'); + // eslint-disable-next-line @gitlab/no-global-event-off $('.dropdown').off('loaded.gl.dropdown'); $(document).off('click', '.js-sidebar-toggle'); }; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index d22aca35e09..18160421136 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -3,6 +3,7 @@ import { __ } from './locale'; function expandSection($section) { $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse')); + // eslint-disable-next-line @gitlab/no-global-event-off $section .find('.settings-content') .off('scroll.expandSection') diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 0e52d2d8010..c4655d35cf0 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -95,6 +95,7 @@ export default class SmartInterval { window.removeEventListener('blur', this.onWindowVisibilityChange); window.removeEventListener('focus', this.onWindowVisibilityChange); this.cancel(); + // eslint-disable-next-line @gitlab/no-global-event-off $(document) .off('visibilitychange') .off('beforeunload'); diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index cf9064aba57..bae320cb705 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -25,6 +25,7 @@ export default class GLTerminal { this.setSocketUrl(); this.createTerminal(); + // eslint-disable-next-line @gitlab/no-global-event-off $(window) .off('resize.terminal') .on('resize.terminal', () => { @@ -104,6 +105,7 @@ export default class GLTerminal { } dispose() { + // eslint-disable-next-line @gitlab/no-global-event-off this.terminal.off('data'); this.terminal.dispose(); this.socket.close(); diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js index ec515e892c6..4e00e0f11f7 100644 --- a/app/assets/javascripts/version_check_image.js +++ b/app/assets/javascripts/version_check_image.js @@ -1,5 +1,6 @@ export default class VersionCheckImage { static bindErrorEvent(imageElement) { + // eslint-disable-next-line @gitlab/no-global-event-off imageElement.off('error').on('error', () => imageElement.hide()); } } diff --git a/app/models/concerns/enums/data_visualization_palette.rb b/app/models/concerns/enums/data_visualization_palette.rb new file mode 100644 index 00000000000..25002e64ba6 --- /dev/null +++ b/app/models/concerns/enums/data_visualization_palette.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Enums + # These color palettes are part of the Pajamas Design System. + # See https://design.gitlab.com/data-visualization/color/#categorical-data + module DataVisualizationPalette + def self.colors + { + blue: 0, + orange: 1, + aqua: 2, + green: 3, + magenta: 4 + } + end + + def self.weights + { + '50' => 0, + '100' => 1, + '200' => 2, + '300' => 3, + '400' => 4, + '500' => 5, + '600' => 6, + '700' => 7, + '800' => 8, + '900' => 9, + '950' => 10 + } + end + end +end diff --git a/changelogs/unreleased/262857-creation-rotation-table-models.yml b/changelogs/unreleased/262857-creation-rotation-table-models.yml new file mode 100644 index 00000000000..d45c60ca289 --- /dev/null +++ b/changelogs/unreleased/262857-creation-rotation-table-models.yml @@ -0,0 +1,5 @@ +--- +title: Add oncall rotations and participants tables +merge_request: 49058 +author: +type: added diff --git a/changelogs/unreleased/group-member-webhook-column.yml b/changelogs/unreleased/group-member-webhook-column.yml new file mode 100644 index 00000000000..fef9556be23 --- /dev/null +++ b/changelogs/unreleased/group-member-webhook-column.yml @@ -0,0 +1,5 @@ +--- +title: Add member_events column to web_hooks table +merge_request: 49273 +author: +type: added diff --git a/changelogs/unreleased/sh-handle-zero-maximum-upload-size.yml b/changelogs/unreleased/sh-handle-zero-maximum-upload-size.yml new file mode 100644 index 00000000000..7781f994641 --- /dev/null +++ b/changelogs/unreleased/sh-handle-zero-maximum-upload-size.yml @@ -0,0 +1,5 @@ +--- +title: Fix division by error when upload max size is set to 0 +merge_request: 49482 +author: +type: fixed diff --git a/db/migrate/20201124030537_create_incident_management_on_call_rotations.rb b/db/migrate/20201124030537_create_incident_management_on_call_rotations.rb new file mode 100644 index 00000000000..18546d97fd5 --- /dev/null +++ b/db/migrate/20201124030537_create_incident_management_on_call_rotations.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateIncidentManagementOnCallRotations < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless table_exists?(:incident_management_oncall_rotations) + with_lock_retries do + create_table :incident_management_oncall_rotations do |t| + t.timestamps_with_timezone + t.references :oncall_schedule, index: false, null: false, foreign_key: { to_table: :incident_management_oncall_schedules, on_delete: :cascade } + t.integer :length, null: false + t.integer :length_unit, limit: 2, null: false + t.datetime_with_timezone :starts_at, null: false + t.text :name, null: false + + t.index %w(oncall_schedule_id id), name: 'index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_id', unique: true, using: :btree + t.index %w(oncall_schedule_id name), name: 'index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_name', unique: true, using: :btree + end + end + end + + add_text_limit :incident_management_oncall_rotations, :name, 200 + end + + def down + with_lock_retries do + drop_table :incident_management_oncall_rotations + end + end +end diff --git a/db/migrate/20201125233219_add_incident_management_on_call_participants.rb b/db/migrate/20201125233219_add_incident_management_on_call_participants.rb new file mode 100644 index 00000000000..2a9b1d8b276 --- /dev/null +++ b/db/migrate/20201125233219_add_incident_management_on_call_participants.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class AddIncidentManagementOnCallParticipants < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + PARTICIPANT_ROTATION_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_oncall_rotation_id' + PARTICIPANT_USER_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_oncall_user_id' + UNIQUE_INDEX_NAME = 'index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id' + + disable_ddl_transaction! + + def up + unless table_exists?(:incident_management_oncall_participants) + with_lock_retries do + create_table :incident_management_oncall_participants do |t| + t.references :oncall_rotation, index: false, null: false, foreign_key: { to_table: :incident_management_oncall_rotations, on_delete: :cascade } + t.references :user, index: false, null: false, foreign_key: { on_delete: :cascade } + t.integer :color_palette, limit: 2, null: false + t.integer :color_weight, limit: 2, null: false + t.index :user_id, name: PARTICIPANT_USER_INDEX_NAME + t.index :oncall_rotation_id, name: PARTICIPANT_ROTATION_INDEX_NAME + t.index [:user_id, :oncall_rotation_id], unique: true, name: UNIQUE_INDEX_NAME + end + end + end + end + + def down + drop_table :incident_management_oncall_participants + end +end diff --git a/db/migrate/20201204205814_add_member_events_to_web_hooks.rb b/db/migrate/20201204205814_add_member_events_to_web_hooks.rb new file mode 100644 index 00000000000..edb374f1bdd --- /dev/null +++ b/db/migrate/20201204205814_add_member_events_to_web_hooks.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddMemberEventsToWebHooks < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column :web_hooks, :member_events, :boolean, null: false, default: false + end +end diff --git a/db/schema_migrations/20201124030537 b/db/schema_migrations/20201124030537 new file mode 100644 index 00000000000..6754e179ae3 --- /dev/null +++ b/db/schema_migrations/20201124030537 @@ -0,0 +1 @@ +2929b74d9b9d6e205c0e1fb2aaaffe323394058f6e583c56035a2c83b4d4ff03
\ No newline at end of file diff --git a/db/schema_migrations/20201125233219 b/db/schema_migrations/20201125233219 new file mode 100644 index 00000000000..54728b704e9 --- /dev/null +++ b/db/schema_migrations/20201125233219 @@ -0,0 +1 @@ +451d7f29392f965467f364c7b119d269551e2dc1485e8cb15ebd14753fdb6e6a
\ No newline at end of file diff --git a/db/schema_migrations/20201204205814 b/db/schema_migrations/20201204205814 new file mode 100644 index 00000000000..2308ba1245c --- /dev/null +++ b/db/schema_migrations/20201204205814 @@ -0,0 +1 @@ +8178b8a9acf7d2d8990bb6f7d984eb9e3b77d45cb2a8b54b56500ef6f93772ad
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c84f5eaec80..0acceb8c9be 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12989,6 +12989,44 @@ CREATE SEQUENCE import_failures_id_seq ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id; +CREATE TABLE incident_management_oncall_participants ( + id bigint NOT NULL, + oncall_rotation_id bigint NOT NULL, + user_id bigint NOT NULL, + color_palette smallint NOT NULL, + color_weight smallint NOT NULL +); + +CREATE SEQUENCE incident_management_oncall_participants_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE incident_management_oncall_participants_id_seq OWNED BY incident_management_oncall_participants.id; + +CREATE TABLE incident_management_oncall_rotations ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + oncall_schedule_id bigint NOT NULL, + length integer NOT NULL, + length_unit smallint NOT NULL, + starts_at timestamp with time zone NOT NULL, + name text NOT NULL, + CONSTRAINT check_5209fb5d02 CHECK ((char_length(name) <= 200)) +); + +CREATE SEQUENCE incident_management_oncall_rotations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE incident_management_oncall_rotations_id_seq OWNED BY incident_management_oncall_rotations.id; + CREATE TABLE incident_management_oncall_schedules ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -17713,7 +17751,8 @@ CREATE TABLE web_hooks ( encrypted_url_iv character varying, deployment_events boolean DEFAULT false NOT NULL, releases_events boolean DEFAULT false NOT NULL, - feature_flag_events boolean DEFAULT false NOT NULL + feature_flag_events boolean DEFAULT false NOT NULL, + member_events boolean DEFAULT false NOT NULL ); CREATE SEQUENCE web_hooks_id_seq @@ -18230,6 +18269,10 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass); +ALTER TABLE ONLY incident_management_oncall_participants ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_participants_id_seq'::regclass); + +ALTER TABLE ONLY incident_management_oncall_rotations ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_rotations_id_seq'::regclass); + ALTER TABLE ONLY incident_management_oncall_schedules ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_schedules_id_seq'::regclass); ALTER TABLE ONLY index_statuses ALTER COLUMN id SET DEFAULT nextval('index_statuses_id_seq'::regclass); @@ -19439,6 +19482,12 @@ ALTER TABLE ONLY import_export_uploads ALTER TABLE ONLY import_failures ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id); +ALTER TABLE ONLY incident_management_oncall_participants + ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY incident_management_oncall_rotations + ADD CONSTRAINT incident_management_oncall_rotations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY incident_management_oncall_schedules ADD CONSTRAINT incident_management_oncall_schedules_pkey PRIMARY KEY (id); @@ -21372,6 +21421,16 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL); +CREATE INDEX index_inc_mgmnt_oncall_participants_on_oncall_rotation_id ON incident_management_oncall_participants USING btree (oncall_rotation_id); + +CREATE INDEX index_inc_mgmnt_oncall_participants_on_oncall_user_id ON incident_management_oncall_participants USING btree (user_id); + +CREATE UNIQUE INDEX index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id ON incident_management_oncall_participants USING btree (user_id, oncall_rotation_id); + +CREATE UNIQUE INDEX index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_id ON incident_management_oncall_rotations USING btree (oncall_schedule_id, id); + +CREATE UNIQUE INDEX index_inc_mgmnt_oncall_rotations_on_oncall_schedule_id_and_name ON incident_management_oncall_rotations USING btree (oncall_schedule_id, name); + CREATE INDEX index_incident_management_oncall_schedules_on_project_id ON incident_management_oncall_schedules USING btree (project_id); CREATE UNIQUE INDEX index_index_statuses_on_project_id ON index_statuses USING btree (project_id); @@ -23744,6 +23803,9 @@ ALTER TABLE ONLY namespace_statistics ALTER TABLE ONLY clusters_applications_elastic_stacks ADD CONSTRAINT fk_rails_026f219f46 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE; +ALTER TABLE ONLY incident_management_oncall_participants + ADD CONSTRAINT fk_rails_032b12996a FOREIGN KEY (oncall_rotation_id) REFERENCES incident_management_oncall_rotations(id) ON DELETE CASCADE; + ALTER TABLE ONLY events ADD CONSTRAINT fk_rails_0434b48643 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -23921,6 +23983,9 @@ ALTER TABLE ONLY saml_group_links ALTER TABLE ONLY group_custom_attributes ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY incident_management_oncall_rotations + ADD CONSTRAINT fk_rails_256e0bc604 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE; + ALTER TABLE ONLY analytics_devops_adoption_snapshots ADD CONSTRAINT fk_rails_25da9a92c0 FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE; @@ -24251,6 +24316,9 @@ ALTER TABLE ONLY resource_weight_events ALTER TABLE ONLY approval_project_rules ADD CONSTRAINT fk_rails_5fb4dd100b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY incident_management_oncall_participants + ADD CONSTRAINT fk_rails_5fe86ea341 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY user_highest_roles ADD CONSTRAINT fk_rails_60f6c325a6 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 13dd742b53c..87ccbe6747c 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -210,7 +210,7 @@ control over how the Pages daemon runs and serves content in your environment. | `external_https` | Configure Pages to bind to one or more secondary IP addresses, serving HTTPS requests. Multiple addresses can be given as an array, along with exact ports, for example `['1.2.3.4', '1.2.3.5:8063']`. Sets value for `listen_https`. | `gitlab_client_http_timeout` | GitLab API HTTP client connection timeout in seconds (default: 10s). | `gitlab_client_jwt_expiry` | JWT Token expiry time in seconds (default: 30s). -| `domain_config_source` | Domain configuration source (default: `disk`) +| `domain_config_source` | Domain configuration source (default: `auto`) | `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab. | `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. | `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. @@ -668,13 +668,14 @@ Pages server. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217912) in GitLab 13.3. GitLab Pages can use different sources to get domain configuration. -The default value is `nil`; however, GitLab Pages will default to `disk`. +The default value is `nil`; however, GitLab Pages will default to `auto`. ```ruby gitlab_pages['domain_config_source'] = nil ``` -You can specify `gitlab` to enable [API-based configuration](#gitlab-api-based-configuration). +If left unchanged, GitLab Pages tries to use any available source (either `gitlab` or `disk`). The +preferred source is `gitlab`, which uses [API-based configuration](#gitlab-api-based-configuration). For more details see this [blog post](https://about.gitlab.com/blog/2020/08/03/how-gitlab-pages-uses-the-gitlab-api-to-serve-content/). @@ -691,10 +692,10 @@ was used prior to GitLab 13.0. Follow these steps to enable it: 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. -If you encounter an issue, you can disable it by choosing `disk` or `nil`: +If you encounter an issue, you can disable it by choosing `disk`: ```ruby -gitlab_pages['domain_config_source'] = nil +gitlab_pages['domain_config_source'] = "disk" ``` For other common issues, see the [troubleshooting section](#failed-to-connect-to-the-internal-gitlab-api) diff --git a/doc/development/agent/local.md b/doc/development/agent/local.md index baa9cc5696b..75d45366238 100644 --- a/doc/development/agent/local.md +++ b/doc/development/agent/local.md @@ -9,45 +9,45 @@ info: To determine the technical writer assigned to the Stage/Group associated w You can run `kas` and `agentk` locally to test the [Kubernetes Agent](index.md) yourself. 1. Create a `cfg.yaml` file from the contents of - [`config_example.yaml`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/pkg/kascfg/config_example.yaml), or this example: - - ```yaml - agent: - listen: - network: tcp - address: 127.0.0.1:8150 - websocket: false - gitops: - poll_period: "10s" - gitlab: - address: http://localhost:3000 - authentication_secret_file: /Users/tkuah/code/ee-gdk/gitlab/.gitlab_kas_secret - ``` + [`config_example.yaml`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/pkg/kascfg/config_example.yaml), or this example: + + ```yaml + agent: + listen: + network: tcp + address: 127.0.0.1:8150 + websocket: false + gitops: + poll_period: "10s" + gitlab: + address: http://localhost:3000 + authentication_secret_file: /Users/tkuah/code/ee-gdk/gitlab/.gitlab_kas_secret + ``` 1. Create a `token.txt`. This is the token for - [the agent you created](../../user/clusters/agent/index.md#create-an-agent-record-in-gitlab). This file must not contain a newline character. You can create the file with this command: + [the agent you created](../../user/clusters/agent/index.md#create-an-agent-record-in-gitlab). This file must not contain a newline character. You can create the file with this command: - ```shell - echo -n "<TOKEN>" > token.txt - ``` + ```shell + echo -n "<TOKEN>" > token.txt + ``` 1. Start the binaries with the following commands: - ```shell - # Need GitLab to start - gdk start - # Stop GDK's version of kas - gdk stop gitlab-k8s-agent + ```shell + # Need GitLab to start + gdk start + # Stop GDK's version of kas + gdk stop gitlab-k8s-agent - # Start kas - bazel run //cmd/kas -- --configuration-file="$(pwd)/cfg.yaml" - ``` + # Start kas + bazel run //cmd/kas -- --configuration-file="$(pwd)/cfg.yaml" + ``` -1. In a new terminal window, run this command to start agentk: +1. In a new terminal window, run this command to start `agentk`: - ```shell - bazel run //cmd/agentk -- --kas-address=grpc://127.0.0.1:8150 --token-file="$(pwd)/token.txt" - ``` + ```shell + bazel run //cmd/agentk -- --kas-address=grpc://127.0.0.1:8150 --token-file="$(pwd)/token.txt" + ``` You can also inspect the [Makefile](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/Makefile) diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md index ebab0e59cc3..95145a75f1f 100644 --- a/doc/development/secure_coding_guidelines.md +++ b/doc/development/secure_coding_guidelines.md @@ -112,21 +112,43 @@ Here `params[:ip]` should not contain anything else but numbers and dots. Howeve In most cases the anchors `\A` for beginning of text and `\z` for end of text should be used instead of `^` and `$`. -## Denial of Service (ReDoS) +## Denial of Service (ReDoS) / Catastrophic Backtracking -[ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) is a possible attack if the attacker knows -or controls the regular expression (regex) used, and is able to enter user input to match against the bad regular expression. +When a regular expression (regex) is used to search for a string and can't find a match, +it may then backtrack to try other possibilities. + +For example when the regex `.*!$` matches the string `hello!`, the `.*` first matches +the entire string but then the `!` from the regex is unable to match because the +character has already been used. In that case, the Ruby regex engine _backtracks_ +one character to allow the `!` to match. + +[ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) +is an attack in which the attacker knows or controls the regular expression used. +The attacker may be able to enter user input that triggers this backtracking behavior in a +way that increases execution time by several orders of magnitude. ### Impact -The resource, for example Unicorn, Puma, or Sidekiq, can be made to hang as it takes a long time to evaluate the bad regex match. +The resource, for example Unicorn, Puma, or Sidekiq, can be made to hang as it takes +a long time to evaluate the bad regex match. The evaluation time may require manual +termination of the resource. ### Examples -GitLab-specific examples can be found in the following merge requests: +Here are some GitLab-specific examples. + +User inputs used to create regular expressions: + +- [User-controlled filename](https://gitlab.com/gitlab-org/gitlab/-/issues/257497) +- [User-controlled domain name](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25314) +- [User-controlled email address](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25122#note_289087459) + +Hardcoded regular expressions with backtracking issues: -- [MR25314](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25314) -- [MR25122](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25122#note_289087459) +- [Repository name validation](https://gitlab.com/gitlab-org/gitlab/-/issues/220019) +- [Link validation](https://gitlab.com/gitlab-org/gitlab/-/issues/218753), and [a bypass](https://gitlab.com/gitlab-org/gitlab/-/issues/273771) +- [Entity name validation](https://gitlab.com/gitlab-org/gitlab/-/issues/289934) +- [Validating color codes](https://gitlab.com/gitlab-org/gitlab/commit/717824144f8181bef524592eab882dd7525a60ef) Consider the following example application, which defines a check using a regular expression. A user entering `user@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!.com` as the email on a form will hang the web server. @@ -141,22 +163,32 @@ class Email < ApplicationRecord def domain_matches errors.add(:email, 'does not match') if email =~ DOMAIN_MATCH end +end ``` ### Mitigation -GitLab has `Gitlab::UntrustedRegexp` which internally uses the [`re2`](https://github.com/google/re2/wiki/Syntax) library. -By utilizing `re2`, we get a strict limit on total execution time, and a smaller subset of available regex features. +#### Ruby + +GitLab has [`Gitlab::UntrustedRegexp`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/untrusted_regexp.rb) + which internally uses the [`re2`](https://github.com/google/re2/wiki/Syntax) library. +`re2` does not support backtracking so we get constant execution time, and a smaller subset of available regex features. All user-provided regular expressions should use `Gitlab::UntrustedRegexp`. For other regular expressions, here are a few guidelines: -- Remove unnecessary backtracking. -- Avoid nested quantifiers if possible. -- Try to be as precise as possible in your regex and avoid the `.` if something else can be used (e.g.: Use `_[^_]+_` instead of `_.*_` to match `_text here_`). +- If there's a clean non-regex solution, such as `String#start_with?`, consider using it +- Ruby supports some advanced regex features like [atomic groups](https://www.regular-expressions.info/atomic.html) +and [possessive quantifiers](https://www.regular-expressions.info/possessive.html) that eleminate backtracking +- Avoid nested quantifiers if possible (for example `(a+)+`) +- Try to be as precise as possible in your regex and avoid the `.` if there's an alternative + - For example, Use `_[^_]+_` instead of `_.*_` to match `_text here_` +- If in doubt, don't hesitate to ping `@gitlab-com/gl-security/appsec` + +#### Go -An example can be found [in this commit](https://gitlab.com/gitlab-org/gitlab/commit/717824144f8181bef524592eab882dd7525a60ef). +Go's [`regexp`](https://golang.org/pkg/regexp/) package uses `re2` and isn't vulnerable to backtracking issues. ## Further Links @@ -466,7 +498,7 @@ where you can't avoid this: characters, for example). - Always use `--` to separate options from arguments. -### Ruby +#### Ruby Consider using `system("command", "arg0", "arg1", ...)` whenever you can. This prevents an attacker from concatenating commands. @@ -475,7 +507,7 @@ For more examples on how to use shell commands securely, consult [Guidelines for shell commands in the GitLab codebase](shell_commands.md). It contains various examples on how to securely call OS commands. -### Go +#### Go Go has built-in protections that usually prevent an attacker from successfully injecting OS commands. diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 5c34e854d1a..0ce8888272a 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -56,6 +56,12 @@ There are several components that work in concert for the Agent to accomplish Gi These repositories might be the same GitLab project or separate projects. +NOTE: +GitLab recommends you use the same GitLab project for the agent configuration +and manifest repositories. Our backlog contains issues for adding support for +[private manifest repositories outside of the configuration project](https://gitlab.com/gitlab-org/gitlab/-/issues/220912) and +[group level agents](https://gitlab.com/gitlab-org/gitlab/-/issues/283885). + For more details, please refer to our [full architecture documentation](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/architecture.md#high-level-architecture) in the Agent project. ## Get started with GitOps and the GitLab Agent diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 03abf769392..c0c7eaf6ca1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -45,17 +45,17 @@ versions at any given time. We regularly review the versions we support, and provide a three-month deprecation period before we remove support of a specific version. The range of supported versions is based on the evaluation of: -- Our own needs. - The versions supported by major managed Kubernetes providers. - The versions [supported by the Kubernetes community](https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions). GitLab supports the following Kubernetes versions, and you can upgrade your Kubernetes version to any supported version at any time: -- 1.18 -- 1.17 -- 1.16 -- 1.15 +- 1.19 (support ends on February 22, 2022) +- 1.18 (support ends on November 22, 2021) +- 1.17 (support ends on September 22, 2021) +- 1.16 (support ends on July 22, 2021) +- 1.15 (support ends on May 22, 2021) - 1.14 (deprecated, support ends on December 22, 2020) Some GitLab features may support versions outside the range provided here. diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 43785d165fb..0f2fd01e3c7 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -115,4 +115,10 @@ module Gitlab 'web' end + + def self.maintenance_mode? + return false unless ::Feature.enabled?(:maintenance_mode) + + ::Gitlab::CurrentSettings.maintenance_mode + end end diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index b5864382299..3a8fa51e198 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -184,15 +184,20 @@ module ObjectStorage private def rounded_multipart_part_size - # round multipart_part_size up to minimum_mulitpart_size + # round multipart_part_size up to minimum_multipart_size (multipart_part_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE * MINIMUM_MULTIPART_SIZE end def multipart_part_size + return MINIMUM_MULTIPART_SIZE if maximum_size == 0 + maximum_size / number_of_multipart_parts end def number_of_multipart_parts + # If we don't have max length, we can only assume the file is as large as possible. + return MAXIMUM_MULTIPART_PARTS if maximum_size == 0 + [ # round maximum_size up to minimum_mulitpart_size (maximum_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE, @@ -201,7 +206,7 @@ module ObjectStorage end def requires_multipart_upload? - config.aws? && !has_length + config.aws? && !has_length && !use_workhorse_s3_client? end def upload_id diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 8e6faa90c58..d0e585e844a 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -25,3 +25,6 @@ rules: - 'testAction' jest/no-test-callback: - off + "@gitlab/no-global-event-off": + - off + diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js index 7a48353af94..cee9969d26a 100644 --- a/spec/frontend/issue_show/issue_spec.js +++ b/spec/frontend/issue_show/issue_spec.js @@ -30,7 +30,8 @@ describe('Issue show index', () => { initialDescriptionHtml: '<svg onload=window.alert(1)>', }); - const issuableData = parseData.parseIssuableData(); + const initialDataEl = document.getElementById('js-issuable-app'); + const issuableData = parseData.parseIssuableData(initialDataEl); initIssuableApp(issuableData, createStore()); await waitForPromises(); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 8f6ae2b5af7..d882490c272 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -12,6 +12,16 @@ job1: - echo 'test' `; +export const mockCiConfigQueryResponse = { + data: { + ciConfig: { + errors: [], + stages: [], + status: '', + }, + }, +}; + export const mockLintResponse = { valid: true, errors: [], diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index c6dafc6e258..ca54c97d2bb 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -13,9 +13,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import VueApollo from 'vue-apollo'; import createMockApollo from 'jest/helpers/mock_apollo_helper'; -import { redirectTo, refreshCurrentPage, objectToQuery } from '~/lib/utils/url_utility'; +import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; import { mockCiConfigPath, + mockCiConfigQueryResponse, mockCiYml, mockCommitId, mockCommitMessage, @@ -24,10 +25,11 @@ import { mockNewMergeRequestPath, } from './mock_data'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; +import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import getCiConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue'; -import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue'; +import TextEditor from '~/pipeline_editor/components/text_editor.vue'; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -42,9 +44,10 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { let wrapper; - let mockMutate; let mockApollo; let mockBlobContentData; + let mockCiConfigData; + let mockMutate; const createComponent = ({ props = {}, @@ -96,7 +99,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { }; const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { - mockApollo = createMockApollo([], { + const handlers = [[getCiConfig, mockCiConfigData]]; + const resolvers = { Query: { blobContent() { return { @@ -105,7 +109,9 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { }; }, }, - }); + }; + + mockApollo = createMockApollo(handlers, resolvers); const options = { localVue, @@ -125,10 +131,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { beforeEach(() => { mockBlobContentData = jest.fn(); + mockCiConfigData = jest.fn().mockResolvedValue(mockCiConfigQueryResponse); }); afterEach(() => { mockBlobContentData.mockReset(); + mockCiConfigData.mockReset(); refreshCurrentPage.mockReset(); redirectTo.mockReset(); mockMutate.mockReset(); @@ -177,12 +185,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { beforeEach(async () => { createComponent({ mountFn: mount }); - wrapper.setData({ + await wrapper.setData({ content: mockCiYml, contentModel: mockCiYml, }); - - await nextTick(); }); it('displays content after the query loads', () => { @@ -347,7 +353,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => { }); describe('displays fetch content errors', () => { - it('no error is show when data is set', async () => { + it('no error is shown when data is set', async () => { mockBlobContentData.mockResolvedValue(mockCiYml); createComponentWithApollo(); diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index eaab39291b2..e1b8323eb8e 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -329,4 +329,24 @@ RSpec.describe Gitlab do expect(described_class.http_proxy_env?).to eq(false) end end + + describe '.maintenance_mode?' do + it 'returns true when maintenance mode is enabled' do + stub_application_setting(maintenance_mode: true) + + expect(described_class.maintenance_mode?).to eq(true) + end + + it 'returns false when maintenance mode is disabled' do + stub_application_setting(maintenance_mode: false) + + expect(described_class.maintenance_mode?).to eq(false) + end + + it 'returns false when maintenance mode feature flag is disabled' do + stub_feature_flags(maintenance_mode: false) + + expect(described_class.maintenance_mode?).to eq(false) + end + end end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 2af10f9cfe9..bd9d197afa0 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -162,6 +162,10 @@ RSpec.describe ObjectStorage::DirectUpload do it 'enables the Workhorse client' do expect(subject[:UseWorkhorseClient]).to be true end + + it 'omits the multipart upload URLs' do + expect(subject).not_to include(:MultipartUpload) + end end context 'when only server side encryption is used' do @@ -340,6 +344,30 @@ RSpec.describe ObjectStorage::DirectUpload do stub_object_storage_multipart_init(storage_url, "myUpload") end + context 'when maximum upload size is 0' do + let(:maximum_size) { 0 } + + it 'returns maximum number of parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) + end + + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + + context 'when maximum upload size is < 5 MB' do + let(:maximum_size) { 1024 } + + it 'returns only 1 part' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(1) + end + + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + context 'when maximum upload size is 10MB' do let(:maximum_size) { 10.megabyte } |