diff options
107 files changed, 1400 insertions, 872 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8b00bbb192d..e4c321643bb 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -be4a52de3497779aa112614dbad096504cfd07d7 +edca8ea99b1670990281775a5d99429c4492fd8e @@ -421,7 +421,7 @@ group :development, :test do end group :development, :test, :danger do - gem 'gitlab-dangerfiles', '~> 3.6.3', require: false + gem 'gitlab-dangerfiles', '~> 3.6.4', require: false end group :development, :test, :coverage do diff --git a/Gemfile.checksum b/Gemfile.checksum index 9c3173f19f4..c1f0dfddf85 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -201,7 +201,7 @@ {"name":"gitaly","version":"15.5.2","platform":"ruby","checksum":"62babe0596a4505bf95051ea50f17160055e6cf6cacf209273691542120d7881"}, {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"}, {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"}, -{"name":"gitlab-dangerfiles","version":"3.6.3","platform":"ruby","checksum":"c696893dcadd99cd1f2074a335085f64bd601ff5778f3c704c98fd6c5ba23e88"}, +{"name":"gitlab-dangerfiles","version":"3.6.4","platform":"ruby","checksum":"864ea24440349ef233ede0d767537d33be4e3c719b298dfd3244b70b4d01756c"}, {"name":"gitlab-experiment","version":"0.7.1","platform":"ruby","checksum":"166dddb3aa83428bcaa93c35684ed01dc4d61f321fd2ae40b020806dc54a7824"}, {"name":"gitlab-fog-azure-rm","version":"1.4.0","platform":"ruby","checksum":"af4163c32b028aa5208814a3f4765a5817d50527e6c61931f766bf18a2e0eb7e"}, {"name":"gitlab-labkit","version":"0.29.0","platform":"ruby","checksum":"eb19ac5c11698683775ab847a3441d7af87d72fbaec38d635149fb65c5d9b427"}, diff --git a/Gemfile.lock b/Gemfile.lock index f12157122f9..8f190979cf2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -557,7 +557,7 @@ GEM terminal-table (>= 1.5.1) gitlab-chronic (0.10.5) numerizer (~> 0.2) - gitlab-dangerfiles (3.6.3) + gitlab-dangerfiles (3.6.4) danger (>= 8.4.5) danger-gitlab (>= 8.0.0) rake @@ -1659,7 +1659,7 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly (~> 15.5.2) gitlab-chronic (~> 0.10.5) - gitlab-dangerfiles (~> 3.6.3) + gitlab-dangerfiles (~> 3.6.4) gitlab-experiment (~> 0.7.1) gitlab-fog-azure-rm (~> 1.4.0) gitlab-labkit (~> 0.29.0) diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index a08cf48c327..50430f8e607 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -5,6 +5,7 @@ import { renderKroki } from './render_kroki'; import renderMath from './render_math'; import renderSandboxedMermaid from './render_sandboxed_mermaid'; import renderMetrics from './render_metrics'; +import renderObservability from './render_observability'; import { renderJSONTable } from './render_json_table'; // Render GitLab flavoured Markdown @@ -32,6 +33,7 @@ $.fn.renderGFM = function renderGFM() { } renderMetrics(this.find('.js-render-metrics').get()); + renderObservability(this.find('.js-render-observability').get()); return this; }; diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js new file mode 100644 index 00000000000..704d85cf22e --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/render_observability.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import { darkModeEnabled } from '~/lib/utils/color_utils'; +import { setUrlParams } from '~/lib/utils/url_utility'; + +export function getFrameSrc(url) { + return `${setUrlParams({ theme: darkModeEnabled() ? 'dark' : 'light' }, url)}&kiosk`; +} + +const mountVueComponent = (element) => { + const url = [element.dataset.frameUrl]; + + return new Vue({ + el: element, + render(h) { + return h('iframe', { + style: { + height: '366px', + width: '768px', + }, + attrs: { + src: getFrameSrc(url), + frameBorder: '0', + }, + }); + }, + }); +}; + +export default function renderObservability(elements) { + elements.forEach((element) => { + mountVueComponent(element); + }); +} diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index a270d9ef03c..7f662f528d7 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -1,10 +1,8 @@ <script> import { GlLoadingIcon, GlIcon, GlTabs, GlTab, GlBadge, GlAlert } from '@gitlab/ui'; -import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; import SafeHtml from '~/vue_shared/directives/safe_html'; import IDEServices from '~/ide/services'; -import { sprintf, __ } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import JobsList from '../jobs/list.vue'; import EmptyState from './empty_state.vue'; @@ -41,16 +39,6 @@ export default { 'stages', 'isLoadingJobs', ]), - ciLintText() { - return sprintf( - __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), - { - linkStart: `<a href="${escape(this.currentProject.web_url)}/-/ci/lint">`, - linkEnd: '</a>', - }, - false, - ); - }, showLoadingIcon() { return this.isLoadingPipeline && !this.hasLoadedPipeline; }, @@ -94,9 +82,8 @@ export default { :dismissible="false" class="gl-mt-5" > - <p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p> + <p class="gl-mb-0">{{ __('Unable to create pipeline') }}</p> <p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p> - <p v-safe-html="ciLintText" class="gl-mb-0"></p> </gl-alert> <gl-tabs v-else> <gl-tab :active="!pipelineFailed"> diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue index 25d4037bbe5..f351a9a392f 100644 --- a/app/assets/javascripts/import_entities/components/group_dropdown.vue +++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue @@ -1,5 +1,21 @@ <script> import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; +import { debounce } from 'lodash'; + +import { s__ } from '~/locale'; +import { createAlert } from '~/flash'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; + +const reportNamespaceLoadError = debounce( + () => + createAlert({ + message: s__('ImportProjects|Requesting namespaces failed'), + }), + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, +); export default { components: { @@ -7,18 +23,32 @@ export default { GlSearchBoxByType, }, inheritAttrs: false, - props: { - namespaces: { - type: Array, - required: true, - }, - }, data() { return { searchTerm: '' }; }, + apollo: { + namespaces: { + query: searchNamespacesWhereUserCanCreateProjectsQuery, + variables() { + return { + search: this.searchTerm, + }; + }, + skip() { + const hasNotEnoughSearchCharacters = + this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH; + return hasNotEnoughSearchCharacters; + }, + update(data) { + return data.currentUser.groups.nodes; + }, + error: reportNamespaceLoadError, + debounce: DEBOUNCE_DELAY, + }, + }, computed: { filteredNamespaces() { - return this.namespaces.filter((ns) => + return (this.namespaces ?? []).filter((ns) => ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()), ); }, diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 66dff77eef8..40eba0a19d2 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -21,12 +21,13 @@ import { getGroupPathAvailability } from '~/rest_api'; import axios from '~/lib/utils/axios_utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { STATUSES } from '../../constants'; import ImportStatusCell from '../../components/import_status.vue'; import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql'; import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql'; -import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql'; import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants'; import { StatusPoller } from '../services/status_poller'; @@ -107,7 +108,12 @@ export default { return { page: this.page, filter: this.filter, perPage: this.perPage }; }, }, - availableNamespaces: availableNamespacesQuery, + availableNamespaces: { + query: searchNamespacesWhereUserCanCreateProjectsQuery, + update(data) { + return data.currentUser.groups.nodes; + }, + }, }, fields: [ @@ -421,7 +427,7 @@ export default { data: { exists }, } = await getGroupPathAvailability( importTarget.newName, - importTarget.targetNamespace.id, + getIdFromGraphQLId(importTarget.targetNamespace.id), { cancelToken: importTarget.cancellationToken?.token, }, @@ -482,9 +488,13 @@ export default { validationErrors: [], }); - getGroupPathAvailability(importTarget.newName, importTarget.targetNamespace.id, { - cancelToken: cancellationToken.token, - }) + getGroupPathAvailability( + importTarget.newName, + getIdFromGraphQLId(importTarget.targetNamespace.id), + { + cancelToken: cancellationToken.token, + }, + ) .then(({ data: { exists, suggests: suggestions } }) => { if (!exists) return; @@ -692,7 +702,6 @@ export default { <template #cell(importTarget)="{ item: group }"> <import-target-cell :group="group" - :available-namespaces="availableNamespaces" :group-path-regex="groupPathRegex" @update-target-namespace="updateImportTarget(group, { targetNamespace: $event })" @update-new-name="updateImportTarget(group, { newName: $event })" diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue index 4fbbd5b239c..04a90d9c20c 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue @@ -22,10 +22,6 @@ export default { type: Object, required: true, }, - availableNamespaces: { - type: Array, - required: true, - }, }, computed: { @@ -53,7 +49,6 @@ export default { #default="{ namespaces }" :text="fullPath" :disabled="!group.flags.isAvailableForImport" - :namespaces="availableNamespaces" toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!" class="gl-h-7 gl-flex-grow-1" data-qa-selector="target_namespace_selector_dropdown" diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index 36da996ea17..913a5a659b3 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -10,7 +10,6 @@ import typeDefs from './typedefs.graphql'; export const clientTypenames = { BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroup: 'ClientBulkImportSourceGroup', - AvailableNamespace: 'ClientAvailableNamespace', BulkImportPageInfo: 'ClientBulkImportPageInfo', BulkImportTarget: 'ClientBulkImportTarget', BulkImportProgress: 'ClientBulkImportProgress', @@ -110,15 +109,6 @@ export function createResolvers({ endpoints }) { }; return response; }, - - availableNamespaces: () => - axios.get(endpoints.availableNamespaces).then(({ data }) => - data.map((namespace) => ({ - __typename: clientTypenames.AvailableNamespace, - id: namespace.id, - fullPath: namespace.full_path, - })), - ), }, Mutation: { async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) { diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql deleted file mode 100644 index b0741dfbe5c..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query availableNamespaces { - availableNamespaces @client { - id - fullPath - } -} diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js index 5d7e7911f5a..494a845b1f9 100644 --- a/app/assets/javascripts/import_entities/import_groups/index.js +++ b/app/assets/javascripts/import_entities/import_groups/index.js @@ -12,7 +12,6 @@ export function mountImportGroupsApp(mountElement) { const { statusPath, - availableNamespacesPath, createBulkImportPath, jobsPath, historyPath, @@ -25,7 +24,6 @@ export function mountImportGroupsApp(mountElement) { sourceUrl, endpoints: { status: statusPath, - availableNamespaces: availableNamespacesPath, createBulkImport: createBulkImportPath, }, }), diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue index 97a7ed4bf55..701384f14a7 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue @@ -58,9 +58,8 @@ export default { }, computed: { - ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']), + ...mapState(['filter', 'repositories', 'defaultTargetNamespace', 'pageInfo', 'isLoadingRepos']), ...mapGetters([ - 'isLoading', 'isImportingAnyRepo', 'importingRepoCount', 'hasImportableRepos', @@ -196,7 +195,6 @@ export default { <provider-repo-table-row :key="repo.importSource.providerLink" :repo="repo" - :available-namespaces="namespaces" :user-namespace="defaultTargetNamespace" :optional-stages="optionalStagesSelection" /> @@ -209,9 +207,9 @@ export default { :key="pagePaginationStateKey" @appear="fetchRepos" /> - <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="lg" /> + <gl-loading-icon v-if="isLoadingRepos" class="gl-mt-7" size="lg" /> - <div v-if="!isLoading && repositories.length === 0" class="gl-text-center"> + <div v-if="!isLoadingRepos && repositories.length === 0" class="gl-text-center"> <strong>{{ emptyStateText }}</strong> </div> </div> diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue index 458e0fb1cb1..371373d9f2b 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue @@ -39,10 +39,6 @@ export default { type: String, required: true, }, - availableNamespaces: { - type: Array, - required: true, - }, optionalStages: { type: Object, required: true, @@ -127,11 +123,7 @@ export default { <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template> <template v-else-if="isImportNotStarted"> <div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full"> - <import-group-dropdown - #default="{ namespaces }" - :text="importTarget.targetNamespace" - :namespaces="availableNamespaces" - > + <import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace"> <template v-if="namespaces.length"> <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header> <gl-dropdown-item diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js index df26d6ac4f6..c7e5a424add 100644 --- a/app/assets/javascripts/import_entities/import_projects/index.js +++ b/app/assets/javascripts/import_entities/import_projects/index.js @@ -1,10 +1,14 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '~/vue_shared/translate'; +import createDefaultClient from '~/lib/graphql'; import ImportProjectsTable from './components/import_projects_table.vue'; + import createStore from './store'; Vue.use(Translate); +Vue.use(VueApollo); export function initStoreFromElement(element) { const { @@ -15,7 +19,6 @@ export function initStoreFromElement(element) { reposPath, jobsPath, importPath, - namespacesPath, defaultTargetNamespace, paginatable, } = element.dataset; @@ -31,7 +34,6 @@ export function initStoreFromElement(element) { reposPath, jobsPath, importPath, - namespacesPath, }, hasPagination: parseBoolean(paginatable), }); @@ -46,6 +48,12 @@ export function initPropsFromElement(element) { }; } +const defaultClient = createDefaultClient(); + +const apolloProvider = new VueApollo({ + defaultClient, +}); + export default function mountImportProjectsTable(mountElement) { if (!mountElement) return undefined; @@ -55,6 +63,7 @@ export default function mountImportProjectsTable(mountElement) { return new Vue({ el: mountElement, store, + apolloProvider, render(createElement) { return createElement(ImportProjectsTable, { props }); }, diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js index a30c14f9d28..74723ec0e9d 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/actions.js +++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js @@ -176,22 +176,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d }); }; -const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => { - commit(types.REQUEST_NAMESPACES); - axios - .get(namespacesPath) - .then(({ data }) => - commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), - ) - .catch(() => { - createAlert({ - message: s__('ImportProjects|Requesting namespaces failed'), - }); - - commit(types.RECEIVE_NAMESPACES_ERROR); - }); -}; - const setFilter = ({ commit, dispatch }, filter) => { commit(types.SET_FILTER, filter); @@ -208,5 +192,4 @@ export default ({ endpoints = isRequired() }) => ({ fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }), fetchImport: fetchImportFactory(endpoints.importPath), fetchJobs: fetchJobsFactory(endpoints.jobsPath), - fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath), }); diff --git a/app/assets/javascripts/import_entities/import_projects/store/getters.js b/app/assets/javascripts/import_entities/import_projects/store/getters.js index ef01a67ec94..31ddffd4eb4 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/getters.js +++ b/app/assets/javascripts/import_entities/import_projects/store/getters.js @@ -1,7 +1,5 @@ import { isProjectImportable, isIncompatible, isImporting } from '../utils'; -export const isLoading = (state) => state.isLoadingRepos || state.isLoadingNamespaces; - export const importingRepoCount = (state) => state.repositories.filter(isImporting).length; export const isImportingAnyRepo = (state) => state.repositories.some(isImporting); diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js index 6adf5e59cff..43dcd54c239 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js +++ b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js @@ -2,10 +2,6 @@ export const REQUEST_REPOS = 'REQUEST_REPOS'; export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS'; export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR'; -export const REQUEST_NAMESPACES = 'REQUEST_NAMESPACES'; -export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS'; -export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR'; - export const REQUEST_IMPORT = 'REQUEST_IMPORT'; export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS'; export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js index 163a19976de..fda0ed47c0f 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js +++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js @@ -122,19 +122,6 @@ export default { }); }, - [types.REQUEST_NAMESPACES](state) { - state.isLoadingNamespaces = true; - }, - - [types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) { - state.isLoadingNamespaces = false; - state.namespaces = namespaces; - }, - - [types.RECEIVE_NAMESPACES_ERROR](state) { - state.isLoadingNamespaces = false; - }, - [types.SET_IMPORT_TARGET](state, { repoId, importTarget }) { const existingRepo = state.repositories.find((r) => r.importSource.id === repoId); diff --git a/app/assets/javascripts/import_entities/import_projects/store/state.js b/app/assets/javascripts/import_entities/import_projects/store/state.js index ecd93561d52..010f462e385 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/state.js +++ b/app/assets/javascripts/import_entities/import_projects/store/state.js @@ -1,10 +1,8 @@ export default () => ({ provider: '', repositories: [], - namespaces: [], customImportTargets: {}, isLoadingRepos: false, - isLoadingNamespaces: false, ciCdOnly: false, filter: '', pageInfo: { diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue index 5e2bd096534..b9d876ef72f 100644 --- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue +++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue @@ -13,13 +13,31 @@ import { urlSortParams, } from '~/issues/list/constants'; import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql'; -import { getInitialPageParams, getSortKey, getSortOptions, isSortKey } from '~/issues/list/utils'; +import { + convertToApiParams, + convertToSearchQuery, + convertToUrlParams, + getFilterTokens, + getInitialPageParams, + getSortKey, + getSortOptions, + isSortKey, +} from '~/issues/list/utils'; +import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; import { getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; +import { + TOKEN_TITLE_ASSIGNEE, + TOKEN_TITLE_AUTHOR, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, +} from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; +const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue'); + export default { i18n: { calendarButtonText: __('Subscribe to calendar'), @@ -66,11 +84,11 @@ export default { const sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey; return { + filterTokens: getFilterTokens(window.location.search), issues: [], issuesError: null, pageInfo: {}, pageParams: getInitialPageParams(), - searchTokens: [], sortKey, state: state || IssuableStates.Opened, }; @@ -82,9 +100,11 @@ export default { return { hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn, isSignedIn: this.isSignedIn, + search: this.searchQuery, sort: this.sortKey, state: this.state, ...this.pageParams, + ...this.apiFilterParams, }; }, update(data) { @@ -97,9 +117,52 @@ export default { this.issuesError = this.$options.i18n.errorFetchingIssues; Sentry.captureException(error); }, + debounce: 200, }, }, computed: { + apiFilterParams() { + return convertToApiParams(this.filterTokens); + }, + searchQuery() { + return convertToSearchQuery(this.filterTokens); + }, + searchTokens() { + const preloadedUsers = []; + + if (gon.current_user_id) { + preloadedUsers.push({ + id: gon.current_user_id, + name: gon.current_user_fullname, + username: gon.current_username, + avatar_url: gon.current_user_avatar_url, + }); + } + + const tokens = [ + { + type: TOKEN_TYPE_ASSIGNEE, + title: TOKEN_TITLE_ASSIGNEE, + icon: 'user', + token: UserToken, + fetchUsers: this.fetchUsers, + preloadedUsers, + recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-assignee', + }, + { + type: TOKEN_TYPE_AUTHOR, + title: TOKEN_TITLE_AUTHOR, + icon: 'pencil', + token: UserToken, + fetchUsers: this.fetchUsers, + defaultUsers: [], + preloadedUsers, + recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-author', + }, + ]; + + return tokens; + }, showPaginationControls() { return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage); }, @@ -110,14 +173,22 @@ export default { hasIssueWeightsFeature: this.hasIssueWeightsFeature, }); }, + urlFilterParams() { + return convertToUrlParams(this.filterTokens); + }, urlParams() { return { + search: this.searchQuery, sort: urlSortParams[this.sortKey], state: this.state, + ...this.urlFilterParams, }; }, }, methods: { + fetchUsers(search) { + return axios.get('/-/autocomplete/users.json', { params: { active: true, search } }); + }, getStatus(issue) { if (issue.state === IssuableStatus.Closed && issue.moved) { return this.$options.i18n.closedMoved; @@ -131,12 +202,16 @@ export default { if (this.state === state) { return; } - this.pageParams = getInitialPageParams(); this.state = state; + this.pageParams = getInitialPageParams(); }, handleDismissAlert() { this.issuesError = null; }, + handleFilter(tokens) { + this.filterTokens = tokens; + this.pageParams = getInitialPageParams(); + }, handleNextPage() { this.pageParams = { afterCursor: this.pageInfo.endCursor, @@ -156,8 +231,8 @@ export default { return; } - this.pageParams = getInitialPageParams(); this.sortKey = sortKey; + this.pageParams = getInitialPageParams(); if (this.isSignedIn) { this.saveSortPreference(sortKey); @@ -189,6 +264,7 @@ export default { :has-next-page="pageInfo.hasNextPage" :has-previous-page="pageInfo.hasPreviousPage" :has-scoped-labels-feature="hasScopedLabelsFeature" + :initial-filter-value="filterTokens" :initial-sort-by="sortKey" :issuables="issues" :issuables-loading="$apollo.queries.issues.loading" @@ -197,12 +273,14 @@ export default { :search-input-placeholder="$options.i18n.searchInputPlaceholder" :search-tokens="searchTokens" :show-pagination-controls="showPaginationControls" + show-work-item-type-icon :sort-options="sortOptions" :tabs="$options.IssuableListTabs" :url-params="urlParams" use-keyset-pagination @click-tab="handleClickTab" @dismiss-alert="handleDismissAlert" + @filter="handleFilter" @next-page="handleNextPage" @previous-page="handlePreviousPage" @sort="handleSort" diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql index 6d0c7139068..8ffcb456755 100644 --- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql @@ -4,16 +4,22 @@ query getDashboardIssues( $hideUsers: Boolean = false $isSignedIn: Boolean = false + $search: String $sort: IssueSort $state: IssuableState + $assigneeUsernames: [String!] + $authorUsername: String $afterCursor: String $beforeCursor: String $firstPageSize: Int $lastPageSize: Int ) { issues( + search: $search sort: $sort state: $state + assigneeUsernames: $assigneeUsernames + authorUsername: $authorUsername after: $afterCursor before: $beforeCursor first: $firstPageSize diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index b4066fff3c6..12a83f06453 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -286,7 +286,7 @@ export default { return convertToUrlParams(this.filterTokens); }, searchQuery() { - return convertToSearchQuery(this.filterTokens) || undefined; + return convertToSearchQuery(this.filterTokens); }, searchTokens() { const preloadedUsers = []; diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js index b20827a22c7..b566e08731c 100644 --- a/app/assets/javascripts/issues/list/utils.js +++ b/app/assets/javascripts/issues/list/utils.js @@ -330,4 +330,4 @@ export const convertToSearchQuery = (filterTokens) => filterTokens .filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data) .map((token) => token.value.data) - .join(' '); + .join(' ') || undefined; diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 4c5f8ed3cba..825a89daf58 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -405,6 +405,11 @@ export const ISSUABLE_TYPES = { INCIDENT: 'incident', }; +export const MILESTONE_STATE = { + ACTIVE: 'active', + CLOSED: 'closed', +}; + export const SEVERITY_I18N = { UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'), TRY_AGAIN: __('Please try again'), diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue index 2cb10d4ae23..0d8a883972f 100644 --- a/app/assets/javascripts/terraform/components/init_command_modal.vue +++ b/app/assets/javascripts/terraform/components/init_command_modal.vue @@ -39,11 +39,13 @@ export default { }, methods: { getModalInfoCopyStr() { + const stateNameEncoded = encodeURIComponent(this.stateName); + return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN> terraform init \\ - -backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\ - -backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\ - -backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\ + -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\ + -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\ + -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\ -backend-config="username=${this.username}" \\ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\ -backend-config="lock_method=POST" \\ diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js new file mode 100644 index 00000000000..7ed0ab52d54 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js @@ -0,0 +1,31 @@ +import { n__, s__, sprintf } from '~/locale'; + +export const i18n = { + label: s__('ciReport|Code Quality'), + loading: s__('ciReport|Code Quality test metrics results are being parsed'), + error: s__('ciReport|Code Quality failed loading results'), + noChanges: s__(`ciReport|No changes to Code Quality.`), + prependText: s__(`ciReport|in`), + fixed: s__(`ciReport|Fixed`), + pluralReport: (errors) => + sprintf( + n__( + '%{strong_start}%{errors}%{strong_end} point', + '%{strong_start}%{errors}%{strong_end} points', + errors.length, + ), + { + errors: errors.length, + }, + false, + ), + singularReport: (errors) => n__('%d point', '%d points', errors.length), + improvementAndDegradationCopy: (improvement, degradation) => + sprintf( + s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`), + ), + improvedCopy: (improvements) => + sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)), + degradedCopy: (degradations) => + sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)), +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js index fd28df2fa38..02170cc3247 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js @@ -1,54 +1,33 @@ -import { n__, s__, sprintf } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants'; import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants'; +import httpStatusCodes from '~/lib/utils/http_status'; import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { i18n } from './constants'; export default { name: 'WidgetCodeQuality', + enablePolling: true, props: ['codeQuality', 'blobPath'], - i18n: { - label: s__('ciReport|Code Quality'), - loading: s__('ciReport|Code Quality test metrics results are being parsed'), - error: s__('ciReport|Code Quality failed loading results'), - }, + i18n, computed: { - summary() { - const { newErrors, resolvedErrors, errorSummary } = this.collapsedData; - if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) { - const improvements = sprintf( - n__( - '%{strong_start}%{errors}%{strong_end} point', - '%{strong_start}%{errors}%{strong_end} points', - resolvedErrors.length, - ), - { - errors: resolvedErrors.length, - }, - false, - ); + summary(data) { + const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data; - const degradations = sprintf( - n__( - '%{strong_start}%{errors}%{strong_end} point', - '%{strong_start}%{errors}%{strong_end} points', - newErrors.length, - ), - { errors: newErrors.length }, - false, - ); - return sprintf( - s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`), + if (parsingInProgress) { + return i18n.loading; + } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) { + return i18n.improvementAndDegradationCopy( + i18n.pluralReport(resolvedErrors), + i18n.pluralReport(newErrors), ); } else if (errorSummary.resolved >= 1) { - const improvements = n__('%d point', '%d points', resolvedErrors.length); - return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)); + return i18n.improvedCopy(i18n.singularReport(resolvedErrors)); } else if (errorSummary.errored >= 1) { - const degradations = n__('%d point', '%d points', newErrors.length); - return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)); + return i18n.degradedCopy(i18n.singularReport(newErrors)); } - return s__(`ciReport|No changes to Code Quality.`); + return i18n.noChanges; }, statusIcon() { if (this.collapsedData.errorSummary?.errored >= 1) { @@ -59,18 +38,17 @@ export default { }, methods: { fetchCollapsedData() { - return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => { + return axios.get(this.codeQuality).then((response) => { + const { data = {}, status } = response; return { - resolvedErrors: parseCodeclimateMetrics( - values[0].resolved_errors, - this.blobPath.head_path, - ), - newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path), - existingErrors: parseCodeclimateMetrics( - values[0].existing_errors, - this.blobPath.head_path, - ), - errorSummary: values[0].summary, + ...response, + data: { + parsingInProgress: status === httpStatusCodes.NO_CONTENT, + resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path), + newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path), + existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path), + errorSummary: data.summary, + }, }; }); }, @@ -81,7 +59,7 @@ export default { return fullData.push({ text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`, subtext: { - prependText: s__(`ciReport|in`), + prependText: i18n.prependText, text: `${e.file_path}:${e.line}`, href: e.urlPath, }, @@ -95,7 +73,7 @@ export default { return fullData.push({ text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`, subtext: { - prependText: s__(`ciReport|in`), + prependText: i18n.prependText, text: `${e.file_path}:${e.line}`, href: e.urlPath, }, @@ -104,7 +82,7 @@ export default { }, badge: { variant: 'neutral', - text: s__(`ciReport|Fixed`), + text: i18n.fixed, }, }); }); diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 9288cf7a733..1df10562e4b 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -349,7 +349,10 @@ export default { </script> <template> - <user-callout-dismisser :skip-query="!displayVscodeWebIdeCallout" feature-name="vscode_web_ide"> + <user-callout-dismisser + :skip-query="!displayVscodeWebIdeCallout" + feature-name="vscode_web_ide_callout" + > <template #default="{ dismiss, shouldShowCallout }"> <div class="gl-sm-ml-3"> <actions-button diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue index 6a6d8d210e8..80f9be6f59f 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue @@ -13,6 +13,7 @@ import { debounce } from 'lodash'; import Tracking from '~/tracking'; import { s__, __ } from '~/locale'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import { MILESTONE_STATE } from '~/sidebar/constants'; import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import { @@ -140,6 +141,7 @@ export default { return { fullPath: this.fullPath, title: this.searchTerm, + state: MILESTONE_STATE.ACTIVE, first: 20, }; }, diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss index 415eb4a648d..b35f5b38740 100644 --- a/app/assets/stylesheets/page_bundles/todos.scss +++ b/app/assets/stylesheets/page_bundles/todos.scss @@ -59,7 +59,10 @@ } .todo-title { + margin-right: 2.5rem; + @include media-breakpoint-up(sm) { + @include gl-mr-0; @include gl-text-overflow-ellipsis; @include gl-white-space-nowrap; @include gl-overflow-hidden; @@ -97,3 +100,12 @@ .todos-list > .todo a:not(.todo-target-link) { z-index: 11 !important; } + +.todo-actions { + @include gl-absolute; + @include gl-right-0; + + @include media-breakpoint-up(sm) { + @include gl-relative; + } +} diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb index 07e6e7a55d6..0192af25d0f 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -11,20 +11,20 @@ module Types abilities.each { |ability| ability_field(ability) } end - def self.ability_field(ability, **kword_args) + def self.ability_field(ability, **kword_args, &block) define_field_resolver_method(ability) unless resolving_keywords?(kword_args) - permission_field(ability, **kword_args) + permission_field(ability, **kword_args, &block) end - def self.permission_field(name, **kword_args) + def self.permission_field(name, **kword_args, &block) kword_args = kword_args.reverse_merge( name: name, type: GraphQL::Types::Boolean, description: "Indicates the user can perform `#{name}` on this resource", null: false) - field(**kword_args) # rubocop:disable Graphql/Descriptions + field(**kword_args, &block) # rubocop:disable Graphql/Descriptions end def self.define_field_resolver_method(ability) diff --git a/app/graphql/types/permission_types/deployment.rb b/app/graphql/types/permission_types/deployment.rb index 794f7a69c55..fce376552b1 100644 --- a/app/graphql/types/permission_types/deployment.rb +++ b/app/graphql/types/permission_types/deployment.rb @@ -10,3 +10,5 @@ module Types end end end + +Types::PermissionTypes::Deployment.prepend_mod_with('Types::PermissionTypes::Deployment') diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 499db3a8dd3..20ff07e88ba 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -78,8 +78,6 @@ module Ci ref.start_with? 'refs/tags/' end - private - def worker_cron_expression Settings.cron_jobs['pipeline_schedule_worker']['cron'] end diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 0039e627984..3f9353214ee 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -64,7 +64,8 @@ module Users merge_request_settings_moved_callout: 60, new_top_level_group_alert: 61, artifacts_management_page_feedback_banner: 62, - vscode_web_ide: 63 + vscode_web_ide: 63, + vscode_web_ide_callout: 64 } validates :feature_name, diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index 8da2af506c7..49f9225a1d3 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MergeRequestPolicy < IssuablePolicy + condition(:can_approve) { can_approve? } + rule { locked }.policy do prevent :reopen_merge_request end @@ -14,7 +16,7 @@ class MergeRequestPolicy < IssuablePolicy prevent :accept_merge_request end - rule { can?(:update_merge_request) & is_project_member }.policy do + rule { can_approve }.policy do enable :approve_merge_request end @@ -40,6 +42,12 @@ class MergeRequestPolicy < IssuablePolicy rule { can?(:admin_merge_request) }.policy do enable :set_merge_request_metadata end + + private + + def can_approve? + can?(:update_merge_request) && is_project_member? + end end MergeRequestPolicy.prepend_mod_with('MergeRequestPolicy') diff --git a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb index 6c8ccb017e9..a1b9ab5f82e 100644 --- a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb +++ b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb @@ -35,7 +35,7 @@ module Ci def worker_cron strong_memoize(:worker_cron) do - Gitlab::Ci::CronParser.new(worker_cron_expression, Time.zone.name) + Gitlab::Ci::CronParser.new(@schedule.worker_cron_expression, Time.zone.name) end end @@ -50,10 +50,6 @@ module Ci Gitlab::Ci::CronParser.parse_natural("every #{every_x_minutes} minutes", Time.zone.name) end end - - def worker_cron_expression - Settings.cron_jobs['pipeline_schedule_worker']['cron'] - end end end end diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 557f6ebd032..2a49b9c5ad8 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,4 +1,5 @@ - page_title _("Groups") +- add_page_specific_style 'page_bundles/search' .top-area .gl-mt-3.gl-mb-3 diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 950642525db..39aec3fc2c8 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,4 +1,5 @@ - page_title _('Projects') +- add_page_specific_style 'page_bundles/search' - params[:visibility_level] ||= [] .top-area diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index 6d85ff50fbe..96e6a264d8e 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -1,3 +1,5 @@ +- add_page_specific_style 'page_bundles/search' + - if registration_features_can_be_prompted? = render Pajamas::AlertComponent.new(variant: :tip, alert_options: { class: 'gl-my-5' }, diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index ede1dfdac5c..47d7105af0d 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -47,13 +47,13 @@ %span.action-description.gl-font-style-italic< = first_line_in_markdown(todo, :body, 100, is_todo: true, project: todo.project, group: todo.group) - .todo-timestamp.gl-white-space-nowrap.gl-sm-ml-3.gl-mt-2.gl-mb-3.gl-sm-my-0.gl-px-2.gl-sm-px-0 + .todo-timestamp.gl-white-space-nowrap.gl-sm-ml-3.gl-mt-2.gl-mb-2.gl-sm-my-0.gl-px-2.gl-sm-px-0 %span.todo-timestamp.gl-font-sm.gl-text-gray-500 = todo_due_date(todo) #{time_ago_with_tooltip(todo.created_at)} - .todo-actions.gl-sm-ml-3.gl-px-2.gl-sm-px-0 + .todo-actions.gl-mr-4.gl-px-2.gl-sm-px-0.gl-sm-mx-0 - if todo.pending? = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-loading btn-icon gl-display-flex js-done-todo has-tooltip', title: _('Mark as done')}, method: :delete, href: dashboard_todo_path(todo)), 'aria-label' => _('Mark as done') do = gl_loading_icon(inline: true) diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index e92db09aaf1..46b15de5caf 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -13,7 +13,6 @@ #import-projects-mount-element{ data: { provider: provider, provider_title: provider_title, can_select_namespace: current_user.can_select_namespace?.to_s, ci_cd_only: has_ci_cd_only_params?.to_s, - namespaces_path: import_available_namespaces_path, repos_path: url_for([:status, :import, provider, { format: :json }]), jobs_path: url_for([:realtime_changes, :import, provider, { format: :json }]), default_target_namespace: default_namespace_path, diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml index 1c8de23f28f..e1547920708 100644 --- a/app/views/import/bulk_imports/status.html.haml +++ b/app/views/import/bulk_imports/status.html.haml @@ -3,7 +3,6 @@ - page_title _('Import groups') #import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json), - available_namespaces_path: import_available_namespaces_path(format: :json), default_target_namespace: @namespace&.id, create_bulk_import_path: import_bulk_imports_path(format: :json), jobs_path: realtime_changes_import_bulk_imports_path(format: :json), diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 1dba3ed1b94..9b0a81a2f60 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -18,13 +18,10 @@ - if pipeline_has_errors .bs-callout.bs-callout-danger - %h4= _('Found errors in your %{gitlab_ci_yml}:') % { gitlab_ci_yml: '.gitlab-ci.yml' } + %h4= _('Unable to create pipeline') %ul - @pipeline.yaml_errors.split("\n").each do |error| %li= error - - lint_link_url = project_ci_pipeline_editor_path(@project, tab: "LINT_TAB") - - lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url } - = s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe } - else #js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline, @current_user) } diff --git a/config/feature_flags/development/allow_dots_on_tf_state_names.yml b/config/feature_flags/development/allow_dots_on_tf_state_names.yml new file mode 100644 index 00000000000..3bbff808eda --- /dev/null +++ b/config/feature_flags/development/allow_dots_on_tf_state_names.yml @@ -0,0 +1,8 @@ +--- +name: allow_dots_on_tf_state_names +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385597 +milestone: '15.7' +type: development +group: group::configure +default_enabled: false diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 363438849ed..58441b83c7d 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -32,7 +32,8 @@ queues_config_hash = Gitlab::Redis::Queues.params queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' -enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero? +enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero? && + !Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED']) Sidekiq.configure_server do |config| config[:strict] = false diff --git a/data/removals/15_6/15-6-nfs-git-repository-storage.yml b/data/removals/15_6/15-6-nfs-git-repository-storage.yml index fabfa2629eb..95d9298fa4f 100644 --- a/data/removals/15_6/15-6-nfs-git-repository-storage.yml +++ b/data/removals/15_6/15-6-nfs-git-repository-storage.yml @@ -1,14 +1,17 @@ -- title: "NFS as Git repository storage is no longer supported. Migrate to Gitaly Cluster as soon as possible" # (required) Actionable title. e.g., The `confidential` field for a `Note` is deprecated. Use `internal` instead. - announcement_milestone: "14.0" # The milestone when this feature was first announced as deprecated. - announcement_date: "2021-06-22" # The date of the milestone release when this feature was first announced as deprecated - removal_milestone: "15.6" # The milestone when this feature is planned to be removed - removal_date: "2022-11-22" # (optional - may be required in the future) YYYY-MM-DD format - the date of the milestone release when this feature is planned to be removed - breaking_change: false # (required) Change to true if this removal is a breaking change. - reporter: mjwood # (required) GitLab username of the person reporting the removal - stage: create # (required) String value of the stage that the feature was created in. e.g., Growth +- title: "NFS as Git repository storage is no longer supported" + announcement_milestone: "14.0" + announcement_date: "2021-06-22" + removal_milestone: "15.6" + removal_date: "2022-11-22" + breaking_change: false + reporter: mjwood + stage: create issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351243 - body: | # (required) Do not modify this line, instead modify the lines below. - As of November 22, 2022, we are removing support for customers utilizing NFS for Git repository storage. This was originally planned for May 22, 2022, but in an effort to allow continued maturity of Gitaly Cluster, we chose to extend our removal of support date until now. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) for further information. + body: | + As of November 22, 2022, we have removed support for customers using NFS for Git repository storage. This was + originally planned for May 22, 2022, but in an effort to allow continued maturity of Gitaly Cluster, we delayed + our removal of support date until now. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) + for further information. This change in support follows the development deprecation for NFS for Git repository storage that occurred in GitLab 14.0. diff --git a/db/post_migrate/20221210154044_update_active_billable_users_index.rb b/db/post_migrate/20221210154044_update_active_billable_users_index.rb new file mode 100644 index 00000000000..9d306eff16b --- /dev/null +++ b/db/post_migrate/20221210154044_update_active_billable_users_index.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +class UpdateActiveBillableUsersIndex < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + OLD_INDEX_NAME = 'active_billable_users' + NEW_INDEX_NAME = 'index_users_for_active_billable' + TABLE_NAME = 'users' + COLUMNS = %i[id] + OLD_INDEX_FILTER_CONDITION = <<~QUERY + ((state)::text = 'active'::text) AND ((user_type IS NULL) + OR (user_type = ANY (ARRAY[NULL::integer, 6, 4]))) AND ((user_type IS NULL) + OR (user_type <> ALL ('{2,6,1,3,7,8}'::smallint[]))) + QUERY + NEW_INDEX_FILTER_CONDITION = <<~QUERY + ((state)::text = 'active'::text) AND ((user_type IS NULL) + OR (user_type = ANY (ARRAY[NULL::integer, 6, 4]))) AND ((user_type IS NULL) + OR (user_type <> ALL ('{1,2,3,4,5,6,7,8,9,11}'::smallint[]))) + QUERY + + def up + add_concurrent_index(TABLE_NAME, COLUMNS, where: NEW_INDEX_FILTER_CONDITION, name: NEW_INDEX_NAME) + remove_concurrent_index_by_name(TABLE_NAME, OLD_INDEX_NAME) + end + + def down + add_concurrent_index(TABLE_NAME, COLUMNS, where: OLD_INDEX_FILTER_CONDITION, name: OLD_INDEX_NAME) + remove_concurrent_index_by_name(TABLE_NAME, NEW_INDEX_NAME) + end +end diff --git a/db/schema_migrations/20221210154044 b/db/schema_migrations/20221210154044 new file mode 100644 index 00000000000..44dd561a449 --- /dev/null +++ b/db/schema_migrations/20221210154044 @@ -0,0 +1 @@ +6349918b178fb0b110f16f4cff6f64c862b3763c5a401238732f6ac507b7c79d
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1a6287183b8..b0793369b4f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -28008,8 +28008,6 @@ CREATE INDEX product_analytics_events_exper_project_id_collector_tstamp_idx9 ON CREATE INDEX product_analytics_events_experi_project_id_collector_tstamp_idx ON gitlab_partitions_static.product_analytics_events_experimental_00 USING btree (project_id, collector_tstamp); -CREATE INDEX active_billable_users ON users USING btree (id) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[NULL::integer, 6, 4]))) AND ((user_type IS NULL) OR (user_type <> ALL ('{2,6,1,3,7,8}'::smallint[])))); - CREATE INDEX analytics_index_audit_events_part_on_created_at_and_author_id ON ONLY audit_events USING btree (created_at, author_id); CREATE INDEX analytics_index_events_on_created_at_and_author_id ON events USING btree (created_at, author_id); @@ -31400,6 +31398,8 @@ CREATE INDEX index_web_hooks_on_integration_id ON web_hooks USING btree (integra CREATE INDEX index_web_hooks_on_project_id ON web_hooks USING btree (project_id); +CREATE INDEX index_users_for_active_billable ON users USING btree (id) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[NULL::integer, 6, 4]))) AND ((user_type IS NULL) OR (user_type <> ALL ('{1,2,3,4,5,6,7,8,9,11}'::smallint[])))); + CREATE INDEX index_web_hooks_on_project_id_recent_failures ON web_hooks USING btree (project_id, recent_failures); CREATE INDEX index_web_hooks_on_type ON web_hooks USING btree (type); diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md index 0f41cbae923..2cb9bac7af9 100644 --- a/doc/administration/auth/ldap/index.md +++ b/doc/administration/auth/ldap/index.md @@ -364,7 +364,9 @@ This can lead to several confusing issues such as creating links or namespaces w GitLab can automatically lowercase usernames provided by the LDAP server by enabling the configuration option `lowercase_usernames`. By default, this configuration option is `false`. -**Omnibus configuration** +::Tabs + +:::TabTitle Linux package (Omnibus) 1. Edit `/etc/gitlab/gitlab.rb`: @@ -379,7 +381,7 @@ the configuration option `lowercase_usernames`. By default, this configuration o 1. [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. -**Source configuration** +:::TabTitle Self-compiled (source) 1. Edit `config/gitlab.yaml`: @@ -394,6 +396,8 @@ the configuration option `lowercase_usernames`. By default, this configuration o 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. +::EndTabs + ### Disable LDAP web sign in It can be useful to prevent using LDAP credentials through the web UI when @@ -404,7 +408,9 @@ checks like custom 2FA. When LDAP web sign in is disabled, users don't see an **LDAP** tab on the sign-in page. This does not disable using LDAP credentials for Git access. -**Omnibus configuration** +::Tabs + +:::TabTitle Linux package (Omnibus) 1. Edit `/etc/gitlab/gitlab.rb`: @@ -414,7 +420,30 @@ This does not disable using LDAP credentials for Git access. 1. [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. -**Source configuration** +:::TabTitle Helm chart (Kubernetes) + +1. Export the Helm values: + + ```shell + helm get values gitlab > gitlab_values.yaml + ``` + +1. Edit `gitlab_values.yaml`: + + ```yaml + global: + appConfig: + ldap: + preventSignin: true + ``` + +1. Save the file and apply the new values: + + ```shell + helm upgrade -f gitlab_values.yaml gitlab gitlab/gitlab + ``` + +:::TabTitle Self-compiled (source) 1. Edit `config/gitlab.yaml`: @@ -426,6 +455,8 @@ This does not disable using LDAP credentials for Git access. 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. +::EndTabs + ### Use encrypted credentials Instead of having the LDAP integration credentials stored in plaintext in the configuration files, you can optionally @@ -445,7 +476,9 @@ The supported configuration items for the encrypted file are: The encrypted contents can be configured with the [LDAP secret edit Rake command](../../raketasks/ldap.md#edit-secret). -**Omnibus configuration** +::Tabs + +:::TabTitle Linux package (Omnibus) If initially your LDAP configuration looked like: @@ -479,7 +512,7 @@ If initially your LDAP configuration looked like: 1. [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. -**Source configuration** +:::TabTitle Self-compiled (source) If initially your LDAP configuration looked like: @@ -513,6 +546,8 @@ If initially your LDAP configuration looked like: 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. +::EndTabs + ## Updating LDAP DN and email When an LDAP server creates a user in GitLab, the user's LDAP distinguished name (DN) is linked to their GitLab account diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 31477bf2919..1dcced781ce 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -49,6 +49,8 @@ health check manually to get this information and a few more details. #### Health check Rake task +> The use of a custom NTP server was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105514) in GitLab 15.7. + This Rake task can be run on a **Rails** node in the **primary** or **secondary** Geo sites: @@ -80,6 +82,22 @@ All projects are in hashed storage? ... yes Checking Geo ... Finished ``` +You can also specify a custom NTP server using environment variables. For example: + +```shell +export NTP_HOST="ntp.ubuntu.com" +export NTP_TIMEOUT="30" +sudo gitlab-rake gitlab:geo:check +``` + +The following environment variables are supported. + +| Variable | Description | Default value | +| ----------- | ----------- | ------------- | +|`NTP_HOST` | The NTP host. | `pool.ntp.org` | +|`NTP_PORT` | The NTP port the host listens on. |`ntp`| +|`NTP_TIMEOUT`| The NTP timeout in seconds. | The value defined in the `net-ntp` Ruby library ([60 seconds](https://github.com/zencoder/net-ntp/blob/3d0990214f439a5127782e0f50faeaf2c8ca7023/lib/net/ntp/ntp.rb#L6)). | + #### Sync status Rake task Current sync information can be found manually by running this Rake task on any diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index c24664d87f0..32b44552b1a 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -54,9 +54,8 @@ Before deploying Gitaly Cluster, review: If you have: -- Not yet migrated to Gitaly Cluster and want to continue using NFS, remain on the service you are using. NFS is - supported in 14.x releases but is [deprecated](../../update/deprecations.md#nfs-for-git-repository-storage). - Support for storing Git repository data on NFS is scheduled to end for all versions of GitLab on November 22, 2022. +- Not yet migrated to Gitaly Cluster and want to continue using NFS, remain on the service you are using. However, NFS + is [no longer supported](../../update/removals.md#nfs-as-git-repository-storage-is-no-longer-supported). - Not yet migrated to Gitaly Cluster but want to migrate away from NFS, you have two options: - A sharded Gitaly instance. - Gitaly Cluster. @@ -104,35 +103,8 @@ These IOPS values are initial recommendations, and may be adjusted to greater or depending on the scale of your environment's workload. If you’re running the environment on a cloud provider, refer to their documentation about how to configure IOPS correctly. -For repository data, only local storage is supported for Gitaly and Gitaly Cluster for performance and consistency reasons. Alternatives such as -[NFS](#moving-beyond-nfs) or [cloud-based systems](../nfs.md#avoid-using-cloud-based-file-systems) are not supported. - -### Moving beyond NFS - -Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be unavailable starting -November 22, 2022. See our [statement of support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) -for more details. - -[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System) -is not well suited to Git workloads which are CPU and IOPS sensitive. -Specifically: - -- Git is sensitive to file system latency. Some operations require many - read operations. Operations that are fast on block storage can become an order of - magnitude slower. This significantly impacts GitLab application performance. -- NFS performance optimizations that prevent the performance gap between - block storage and NFS being even wider are vulnerable to race conditions. We have observed - [data inconsistencies](https://gitlab.com/gitlab-org/gitaly/-/issues/2589) - in production environments caused by simultaneous writes to different NFS - clients. Data corruption is not an acceptable risk. - -Gitaly Cluster is purpose built to provide reliable, high performance, fault -tolerant Git storage. - -Further reading: - -- Blog post: [The road to Gitaly v1.0 (aka, why GitLab doesn't require NFS for storing Git data anymore)](https://about.gitlab.com/blog/2018/09/12/the-road-to-gitaly-1-0/) -- Blog post: [How we spent two weeks hunting an NFS bug in the Linux kernel](https://about.gitlab.com/blog/2018/11/14/how-we-spent-two-weeks-hunting-an-nfs-bug/) +For repository data, only local storage is supported for Gitaly and Gitaly Cluster for performance and consistency reasons. +Alternatives such as [NFS](../nfs.md) or [cloud-based file systems](../nfs.md#avoid-using-cloud-based-file-systems) are not supported. ## Directly accessing repositories @@ -713,8 +685,3 @@ There are two facets to our efforts to remove direct Git access in GitLab: The second facet presents the only real solution. For this, we developed [Gitaly Cluster](#gitaly-cluster). - -## NFS deprecation notice - -Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be -unavailable beginning November 22, 2022. For further information, see our [NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation) documentation. diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md index 85f35a1b188..972db315268 100644 --- a/doc/administration/nfs.md +++ b/doc/administration/nfs.md @@ -20,47 +20,14 @@ actions that read or write to Git repositories. For steps you can use to test file system performance, see [File System Performance Benchmarking](operations/filesystem_benchmarking.md). -## Gitaly and NFS deprecation +## Gitaly with NFS not supported -Starting with GitLab version 14.0, support for NFS to store Git repository data is deprecated. Technical customer support and engineering support is available for the 14.x releases. Engineering is fixing bugs and security vulnerabilities consistent with our [release and maintenance policy](../policy/maintenance.md#security-releases). +Technical and engineering support for using NFS to store Git repository data is officially at end-of-life. No product +changes or troubleshooting is provided through engineering, security or paid support channels. -Upon the release of GitLab 15.6 technical and engineering support for using NFS to store Git repository data is officially at end-of-life. There are no product changes or troubleshooting provided via Engineering, Security or Paid Support channels after the release date of 15.6, regardless of your GitLab version. - -Until the release of 15.6, for customers running 14.x releases, we continue to help with Git related tickets from customers running one or more Gitaly servers with its data stored on NFS. Examples may include: - -- Performance issues or timeouts accessing Git data -- Commits or branches vanish -- GitLab intermittently returns the wrong Git data (such as reporting that a repository has no branches) - -Assistance is limited to activities like: - -- Verifying developers' workflow uses features like protected branches -- Reviewing GitLab event data from the database to advise if it looks like a force push over-wrote branches -- Verifying that NFS client mount options match our [documented recommendations](#mount-options) -- Analyzing the GitLab Workhorse and Rails logs, and determining that `500` errors being seen in the environment are caused by slow responses from Gitaly - -GitLab support is unable to continue with the investigation if both: - -- The date of the request is on or after the release of GitLab version 15.6. -- Support Engineers and Management determine that all reasonable non-NFS root causes have been exhausted. - -If the issue is reproducible, or if it happens intermittently but regularly, GitLab Support can investigate providing the issue reproduces without the use of NFS. To reproduce without NFS, the affected repositories should be migrated to a different Gitaly shard, such as Gitaly cluster or a standalone Gitaly VM, backed with block storage. - -### Why remove NFS for Git repository data - -{:.no-toc} - -NFS is not well-suited to a workload consisting of many small files, like Git repositories. NFS does provide a number of configuration options designed to improve performance. However, over time, a number of these mount options have proven to result in inconsistencies across multiple nodes mounting the NFS volume, up to and including data loss. Addressing these inconsistencies consume extraordinary development and support engineer time that hamper our ability to develop [Gitaly Cluster](gitaly/praefect.md), our purpose-built solution to addressing the deficiencies of NFS in this environment. - -Gitaly Cluster provides highly-available Git repository storage. If this is not a requirement, single-node Gitaly backed by block storage is a suitable substitute. - -Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be -unavailable from GitLab 15.0. No further enhancements are planned for this feature. - -Read: - -- [Moving beyond NFS](gitaly/index.md#moving-beyond-nfs). -- About the [correct mount options to use](#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). +If an issue is reproducible, or if it happens intermittently but regularly, GitLab Support can investigate providing the +issue can be reproduced without NFS. To reproduce without NFS, migrate the affected repositories to a different Gitaly +shard. For example, a Gitaly Cluster or a standalone Gitaly VM, backed with block storage. ## Known kernel version incompatibilities @@ -397,8 +364,8 @@ sudo ufw allow from <client_ip_address> to any port nfs ### Upgrade to Gitaly Cluster or disable caching if experiencing data loss WARNING: -Engineering support for NFS for Git repositories is deprecated. Read about -[moving beyond NFS](gitaly/index.md#moving-beyond-nfs). +Engineering support for NFS for Git repositories +[is unavailable](../update/removals.md#nfs-as-git-repository-storage-is-no-longer-supported). Customers and users have reported data loss on high-traffic repositories when using NFS for Git repositories. For example, we have seen: diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index f7667ec6783..88913eb1f7f 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -35,7 +35,6 @@ full list of reference architectures, see | GitLab Rails | 3 | 32 vCPU, 28.8 GB memory | `n1-highcpu-32` | `c5.9xlarge` | | Monitoring node | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Object storage<sup>4</sup> | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | <!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix --> <!-- markdownlint-disable MD029 --> @@ -228,9 +227,6 @@ To set up GitLab and its components to accommodate up to 10,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -1760,9 +1756,8 @@ To configure Praefect with TLS: Sidekiq requires connection to the [Redis](#configure-redis), [PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1930,9 +1925,8 @@ run [multiple Sidekiq processes](../sidekiq/extra_sidekiq_processes.md). ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. The following IPs will be used as an example: @@ -2071,7 +2065,6 @@ On each node perform the following: 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the first Omnibus node you configured and add or replace the file of the same name on this server. If this is the first Omnibus node you are configuring then you can skip this step. - 1. To ensure database migrations are only run during reconfigure and not automatically on upgrade, run: ```shell @@ -2082,9 +2075,7 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Confirm the node can connect to Gitaly: ```shell @@ -2210,9 +2201,6 @@ To configure the Monitoring node: ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -2236,7 +2224,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -2255,22 +2243,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index 25280d3a06d..02739904f5e 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -35,7 +35,6 @@ full list of reference architectures, see | GitLab Rails | 5 | 32 vCPU, 28.8 GB memory | `n1-highcpu-32` | `c5.9xlarge` | | Monitoring node | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Object storage<sup>4</sup> | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | <!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix --> <!-- markdownlint-disable MD029 --> @@ -228,9 +227,6 @@ To set up GitLab and its components to accommodate up to 25,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -1778,9 +1774,8 @@ To configure Praefect with TLS: Sidekiq requires connection to the [Redis](#configure-redis), [PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1948,9 +1943,8 @@ run [multiple Sidekiq processes](../sidekiq/extra_sidekiq_processes.md). ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. The following IPs will be used as an example: @@ -2091,7 +2085,6 @@ On each node perform the following: 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the first Omnibus node you configured and add or replace the file of the same name on this server. If this is the first Omnibus node you are configuring then you can skip this step. - 1. To ensure database migrations are only run during reconfigure and not automatically on upgrade, run: ```shell @@ -2102,9 +2095,7 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Confirm the node can connect to Gitaly: ```shell @@ -2229,9 +2220,6 @@ To configure the Monitoring node: ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -2255,7 +2243,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -2274,22 +2262,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index 6814356074d..f41c8e9cb24 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -29,7 +29,6 @@ For a full list of reference architectures, see | GitLab Rails | 2 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | `F8s v2` | | Monitoring node | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | `F2s v2` | | Object storage<sup>4</sup> | - | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` | <!-- markdownlint-disable MD029 --> 1. Can be optionally run on reputable third-party external PaaS PostgreSQL solutions. See [Recommended cloud providers and services](index.md#recommended-cloud-providers-and-services) for more information. @@ -152,9 +151,6 @@ To set up GitLab and its components to accommodate up to 2,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -781,9 +777,7 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Run `sudo gitlab-rake gitlab:gitaly:check` to confirm the node can connect to Gitaly. 1. Tail the logs to see the requests: @@ -931,9 +925,6 @@ running [Prometheus](../monitoring/prometheus/index.md) and ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -958,7 +949,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -977,23 +968,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -For improved performance, [object storage](#configure-the-object-storage), -along with [Gitaly](#configure-gitaly), are recommended over using NFS whenever -possible. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search **(PREMIUM SELF)** You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md index 6c2b97fc33c..008b5ffcc0e 100644 --- a/doc/administration/reference_architectures/3k_users.md +++ b/doc/administration/reference_architectures/3k_users.md @@ -44,7 +44,6 @@ For a full list of reference architectures, see | GitLab Rails | 3 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | | Monitoring node | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage<sup>4</sup> | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | <!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix --> <!-- markdownlint-disable MD029 --> @@ -234,9 +233,6 @@ To set up GitLab and its components to accommodate up to 3,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -1712,9 +1708,8 @@ To configure Praefect with TLS: Sidekiq requires connection to the [Redis](#configure-redis), [PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. The following IPs will be used as an example: @@ -1881,9 +1876,8 @@ run [multiple Sidekiq processes](../sidekiq/extra_sidekiq_processes.md). ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. On each node perform the following: @@ -2022,7 +2016,6 @@ On each node perform the following: 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the first Omnibus node you configured and add or replace the file of the same name on this server. If this is the first Omnibus node you are configuring then you can skip this step. - 1. To ensure database migrations are only run during reconfigure and not automatically on upgrade, run: ```shell @@ -2033,11 +2026,8 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Run `sudo gitlab-rake gitlab:gitaly:check` to confirm the node can connect to Gitaly. - 1. Tail the logs to see the requests: ```shell @@ -2176,9 +2166,6 @@ running [Prometheus](../monitoring/prometheus/index.md) and ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -2202,7 +2189,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -2221,22 +2208,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md index 60c0bf024cc..87d1408b568 100644 --- a/doc/administration/reference_architectures/50k_users.md +++ b/doc/administration/reference_architectures/50k_users.md @@ -35,7 +35,6 @@ full list of reference architectures, see | GitLab Rails | 12 | 32 vCPU, 28.8 GB memory | `n1-highcpu-32` | `c5.9xlarge` | | Monitoring node | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Object storage<sup>4</sup> | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | <!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix --> <!-- markdownlint-disable MD029 --> @@ -228,9 +227,6 @@ To set up GitLab and its components to accommodate up to 50,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -1773,9 +1769,8 @@ To configure Praefect with TLS: Sidekiq requires connection to the [Redis](#configure-redis), [PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1943,9 +1938,8 @@ run [multiple Sidekiq processes](../sidekiq/extra_sidekiq_processes.md). ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. The following IPs will be used as an example: @@ -2093,7 +2087,6 @@ On each node perform the following: 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the first Omnibus node you configured and add or replace the file of the same name on this server. If this is the first Omnibus node you are configuring then you can skip this step. - 1. To ensure database migrations are only run during reconfigure and not automatically on upgrade, run: ```shell @@ -2104,9 +2097,7 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Confirm the node can connect to Gitaly: ```shell @@ -2231,9 +2222,6 @@ To configure the Monitoring node: ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -2257,7 +2245,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -2276,22 +2264,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md index ddc9575efb4..182edb82b5f 100644 --- a/doc/administration/reference_architectures/5k_users.md +++ b/doc/administration/reference_architectures/5k_users.md @@ -41,7 +41,6 @@ costly-to-operate environment by using the | GitLab Rails | 3 | 16 vCPU, 14.4 GB memory | `n1-highcpu-16` | `c5.4xlarge` | | Monitoring node | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage<sup>4</sup> | - | - | - | - | -| NFS server (non-Gitaly) | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | <!-- Disable ordered list rule https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md029---ordered-list-item-prefix --> <!-- markdownlint-disable MD029 --> @@ -231,9 +230,6 @@ To set up GitLab and its components to accommodate up to 5,000 users: environment. 1. [Configure the object storage](#configure-the-object-storage) used for shared data objects. -1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended) - to have shared disk storage service as an alternative to Gitaly or object - storage. 1. [Configure Advanced Search](#configure-advanced-search) (optional) for faster, more advanced code search across your entire GitLab instance. @@ -1709,9 +1705,8 @@ To configure Praefect with TLS: Sidekiq requires connection to the [Redis](#configure-redis), [PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. - `10.6.0.71`: Sidekiq 1 - `10.6.0.72`: Sidekiq 2 @@ -1877,9 +1872,8 @@ run [multiple Sidekiq processes](../sidekiq/extra_sidekiq_processes.md). ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. -Since it's recommended to use [Object storage](#configure-the-object-storage) -over [NFS](#configure-nfs-optional) for data objects, the following examples -include the Object storage configuration. +Because you must use [Object storage](#configure-the-object-storage) instead of NFS for data objects, the following +examples include the Object storage configuration. On each node perform the following: @@ -2021,7 +2015,6 @@ On each node perform the following: 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the first Omnibus node you configured and add or replace the file of the same name on this server. If this is the first Omnibus node you are configuring then you can skip this step. - 1. To ensure database migrations are only run during reconfigure and not automatically on upgrade, run: ```shell @@ -2032,11 +2025,8 @@ On each node perform the following: [GitLab Rails post-configuration](#gitlab-rails-post-configuration) section. 1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. - -1. [Enable incremental logging](#enable-incremental-logging), unless you are using [NFS](#configure-nfs-optional). - +1. [Enable incremental logging](#enable-incremental-logging). 1. Run `sudo gitlab-rake gitlab:gitaly:check` to confirm the node can connect to Gitaly. - 1. Tail the logs to see the requests: ```shell @@ -2175,9 +2165,6 @@ running [Prometheus](../monitoring/prometheus/index.md) and ## Configure the object storage GitLab supports using an object storage service for holding numerous types of data. -It's recommended over [NFS](#configure-nfs-optional) and in general it's better -in larger setups as object storage is typically much more performant, reliable, -and scalable. GitLab has been tested on a number of object storage providers: @@ -2201,7 +2188,7 @@ NOTE: When using the [storage-specific form](../object_storage.md#storage-specific-configuration) in GitLab 14.x and earlier, you should enable [direct upload mode](../../development/uploads/index.md#direct-upload). The previous [background upload](../../development/uploads/index.md#direct-upload) mode, -which was deprecated in 14.9, requires shared storage such as [NFS](#configure-nfs-optional). +which was deprecated in 14.9, requires shared storage such as NFS. Using separate buckets for each data type is the recommended approach for GitLab. This ensures there are no collisions across the various types of data GitLab stores. @@ -2220,22 +2207,6 @@ GitLab Runner returns job logs in chunks which Omnibus GitLab caches temporarily While sharing the job logs through NFS is supported, it's recommended to avoid the need to use NFS by enabling [incremental logging](../job_logs.md#incremental-logging-architecture) (required when no NFS node has been deployed). Incremental logging uses Redis instead of disk space for temporary caching of job logs. -## Configure NFS (optional) - -[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly) -are recommended over NFS wherever possible for improved performance. - -See how to [configure NFS](../nfs.md). - -WARNING: -Engineering support for NFS for Git repositories is deprecated, and [technical support is scheduled to be unavailable](../nfs.md#gitaly-and-nfs-deprecation) -after the release of GitLab 15.6. No further enhancements are planned for this feature. - -Read: - -- [Gitaly and NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation). -- About the [correct mount options to use](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss). - ## Configure Advanced Search You can leverage Elasticsearch and [enable Advanced Search](../../integration/advanced_search/elasticsearch.md) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 87a8deaddd3..ad53715b57c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11994,6 +11994,7 @@ Approval summary of the deployment. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="deploymentpermissionsapprovedeployment"></a>`approveDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `approve_deployment` on this resource. This field can only be resolved for one environment in any single request. | | <a id="deploymentpermissionsdestroydeployment"></a>`destroyDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `destroy_deployment` on this resource. | | <a id="deploymentpermissionsupdatedeployment"></a>`updateDeployment` | [`Boolean!`](#boolean) | Indicates the user can perform `update_deployment` on this resource. | @@ -22798,6 +22799,7 @@ Name of the feature that the callout is for. | <a id="usercalloutfeaturenameenumuser_reached_limit_free_plan_alert"></a>`USER_REACHED_LIMIT_FREE_PLAN_ALERT` | Callout feature name for user_reached_limit_free_plan_alert. | | <a id="usercalloutfeaturenameenumverification_reminder"></a>`VERIFICATION_REMINDER` | Callout feature name for verification_reminder. | | <a id="usercalloutfeaturenameenumvscode_web_ide"></a>`VSCODE_WEB_IDE` | Callout feature name for vscode_web_ide. | +| <a id="usercalloutfeaturenameenumvscode_web_ide_callout"></a>`VSCODE_WEB_IDE_CALLOUT` | Callout feature name for vscode_web_ide_callout. | | <a id="usercalloutfeaturenameenumweb_ide_alert_dismissed"></a>`WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. | | <a id="usercalloutfeaturenameenumweb_ide_ci_environments_guidance"></a>`WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. | diff --git a/doc/topics/autodevops/troubleshooting.md b/doc/topics/autodevops/troubleshooting.md index bd8b40dc500..ef420323b32 100644 --- a/doc/topics/autodevops/troubleshooting.md +++ b/doc/topics/autodevops/troubleshooting.md @@ -40,7 +40,7 @@ The following are possible reasons: If your pipeline fails with the following message: ```plaintext -Found errors in your .gitlab-ci.yml: +Unable to create pipeline jobs:test config key may not be used with `rules`: only ``` diff --git a/doc/update/index.md b/doc/update/index.md index 8dda96c56c5..d838f8dda34 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -764,9 +764,82 @@ for how to proceed. gitlab-psql`): ```sql - select count(*) from background_migration_jobs where class_name = 'MigrateMergeRequestDiffCommitUsers' and status = 0; + select status, count(*) from background_migration_jobs + where class_name = 'MigrateMergeRequestDiffCommitUsers' group by status; ``` + As jobs are completed, the database records change from `0` (pending) to `1`. If the number of + pending jobs doesn't decrease after a while, it's possible that the + `MigrateMergeRequestDiffCommitUsers` background migration jobs have failed. You + can check for errors in the Sidekiq logs: + + ```shell + sudo grep MigrateMergeRequestDiffCommitUsers /var/log/gitlab/sidekiq/current | grep -i error + ``` + + If needed, you can attempt to run the `MigrateMergeRequestDiffCommitUsers` background + migration jobs manually in the [GitLab Rails Console](../administration/operations/rails_console.md). + This can be done using Sidekiq asynchronously, or by using a Rails process directly: + + - Using Sidekiq to schedule jobs asynchronously: + + ```ruby + # For the first run, only attempt to execute 1 migration. If successful, increase + # the limit for subsequent runs + limit = 1 + + jobs = Gitlab::Database::BackgroundMigrationJob.for_migration_class('MigrateMergeRequestDiffCommitUsers').pending.to_a + + pp "#{jobs.length} jobs remaining" + + jobs.first(limit).each do |job| + BackgroundMigrationWorker.perform_in(5.minutes, 'MigrateMergeRequestDiffCommitUsers', job.arguments) + end + ``` + + NOTE: + The queued jobs can be monitored using Sidekiq's admin panel, which can be accessed at the `/admin/sidekiq` endpoint URI. + + - Using a Rails process to run jobs synchronously: + + ```ruby + def process(concurrency: 1) + queue = Queue.new + + Gitlab::Database::BackgroundMigrationJob + .where(class_name: 'MigrateMergeRequestDiffCommitUsers', status: 0) + .each { |job| queue << job } + + concurrency + .times + .map do + Thread.new do + Thread.abort_on_exception = true + + loop do + job = queue.pop(true) + time = Benchmark.measure do + Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers + .new + .perform(*job.arguments) + end + + puts "#{job.id} finished in #{time.real.round(2)} seconds" + rescue ThreadError + break + end + end + end + .each(&:join) + end + + ActiveRecord::Base.logger.level = Logger::ERROR + process + ``` + + NOTE: + When using Rails to execute these background migrations synchronously, make sure that the machine running the process has sufficient resources to handle the task. If the process gets terminated, it's likely due to insufficient memory available. If your SSH session times out after a while, it might be necessary to run the previous code by using a terminal multiplexer like `screen` or `tmux`. + - See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144). - You may see the following error when setting up two factor authentication (2FA) for accounts diff --git a/doc/update/removals.md b/doc/update/removals.md index e4338bae1fc..10d937f0f16 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -65,9 +65,12 @@ As of December 22, 2022, we are removing the Flowdock integration because the se ## Removed in 15.6 -### NFS as Git repository storage is no longer supported. Migrate to Gitaly Cluster as soon as possible +### NFS as Git repository storage is no longer supported -As of November 22, 2022, we are removing support for customers utilizing NFS for Git repository storage. This was originally planned for May 22, 2022, but in an effort to allow continued maturity of Gitaly Cluster, we chose to extend our removal of support date until now. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) for further information. +As of November 22, 2022, we have removed support for customers using NFS for Git repository storage. This was +originally planned for May 22, 2022, but in an effort to allow continued maturity of Gitaly Cluster, we delayed +our removal of support date until now. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) +for further information. This change in support follows the development deprecation for NFS for Git repository storage that occurred in GitLab 14.0. diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 2bb481d9ecf..6629c798cfa 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -339,7 +339,7 @@ custom job: The above `.gitlab-ci.yml` causes a linting error: ```plaintext -Found errors in your .gitlab-ci.yml: +Unable to create pipeline - dependency_scanning job: chosen stage does not exist; available stages are .pre - unit-tests - .post @@ -590,7 +590,7 @@ like [`SAST.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/l the following error may occur, depending on your GitLab CI/CD configuration: ```plaintext -Found errors in your .gitlab-ci.yml: +Unable to create pipeline jobs:sast config key may not be used with `rules`: only/except ``` diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index 3c45f17d423..bdc9f975970 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -57,6 +57,17 @@ module API def remote_state_handler ::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name], lock_id: params[:ID]) end + + def not_found_for_dots? + Feature.disabled?(:allow_dots_on_tf_state_names) && params[:name].include?(".") + end + + # Change the state name to behave like before, https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105674 + # has been introduced. This behavior can be controlled via `allow_dots_on_tf_state_names` FF. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861 + def legacy_state_name! + params[:name] = params[:name].split('.').first + end end desc 'Get a Terraform state by its name' do @@ -74,6 +85,8 @@ module API end route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get do + legacy_state_name! if not_found_for_dots? + remote_state_handler.find_with_lock do |state| no_content! unless state.latest_file && state.latest_file.exists? @@ -98,6 +111,7 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post do authorize! :admin_terraform_state, user_project + legacy_state_name! if not_found_for_dots? data = request.body.read no_content! if data.empty? @@ -126,6 +140,7 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth delete do authorize! :admin_terraform_state, user_project + legacy_state_name! if not_found_for_dots? remote_state_handler.find_with_lock do |state| ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute @@ -157,6 +172,8 @@ module API requires :Path, type: String, desc: 'Terraform path' end post '/lock' do + not_found! if not_found_for_dots? + authorize! :admin_terraform_state, user_project status_code = :ok @@ -200,6 +217,8 @@ module API optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID' end delete '/lock' do + not_found! if not_found_for_dots? + authorize! :admin_terraform_state, user_project remote_state_handler.unlock! diff --git a/lib/banzai/filter/inline_observability_filter.rb b/lib/banzai/filter/inline_observability_filter.rb new file mode 100644 index 00000000000..27b89073a0e --- /dev/null +++ b/lib/banzai/filter/inline_observability_filter.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Banzai + module Filter + class InlineObservabilityFilter < ::Banzai::Filter::InlineEmbedsFilter + # Placeholder element for the frontend to use as an + # injection point for observability. + def create_element(url) + doc.document.create_element( + 'div', + class: 'js-render-observability', + 'data-frame-url': url + ) + end + + # Search params for selecting observability links. + def xpath_search + "descendant-or-self::a[starts-with(@href, '#{Gitlab::Observability.observability_url}')]" + end + + # Creates a new element based on the parameters + # obtained from the target link + def element_to_embed(node) + url = node['href'] + + create_element(url) + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5e34ff8a33c..73065571849 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -37,6 +37,7 @@ module Banzai Filter::CustomEmojiFilter, Filter::TaskListFilter, Filter::InlineDiffFilter, + Filter::InlineObservabilityFilter, Filter::SetDirectionFilter, Filter::SyntaxHighlightFilter # this filter should remain at the end ] diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 51dc7f33cc1..142f0b8dfd8 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -10,7 +10,7 @@ module Gitlab ConfigError = Class.new(StandardError) TIMEOUT_SECONDS = 30.seconds - TIMEOUT_MESSAGE = 'Resolving config took longer than expected' + TIMEOUT_MESSAGE = 'Request timed out when fetching configuration files.' RESCUE_ERRORS = [ Gitlab::Config::Loader::FormatError, diff --git a/lib/gitlab/memory/watchdog.rb b/lib/gitlab/memory/watchdog.rb index fdaaa183ca1..25af5bd781a 100644 --- a/lib/gitlab/memory/watchdog.rb +++ b/lib/gitlab/memory/watchdog.rb @@ -56,7 +56,7 @@ module Gitlab # Configuration for Watchdog, see Gitlab::Memory::Watchdog::Configurator # for examples. def configure - yield @configuration + yield configuration end def call @@ -68,17 +68,27 @@ module Gitlab monitor if Feature.enabled?(:gitlab_memory_watchdog, type: :ops) end - event_reporter.stopped(log_labels) + event_reporter.stopped(log_labels(memwd_reason: @reason).compact) end - def stop + def stop(reason: nil) + @reason = reason @alive = false end private + attr_reader :configuration + + delegate :event_reporter, :monitors, :sleep_time_seconds, to: :configuration + def monitor - @configuration.monitors.call_each do |result| + if monitors.empty? + stop(reason: 'monitors are not configured') + return + end + + monitors.call_each do |result| break unless @alive next unless result.threshold_violated? @@ -87,7 +97,7 @@ module Gitlab next unless result.strikes_exceeded? - @alive = !strike_exceeded_callback(result.monitor_name, result.payload) + strike_exceeded_callback(result.monitor_name, result.payload) end end @@ -96,7 +106,7 @@ module Gitlab Gitlab::Memory::Reports::HeapDump.enqueue! - handler.call + stop(reason: 'successfully handled') if handler.call end def handler @@ -104,15 +114,7 @@ module Gitlab # all that happens is we collect logs and Prometheus events for fragmentation violations. return NullHandler.instance unless Feature.enabled?(:enforce_memory_watchdog, type: :ops) - @configuration.handler - end - - def event_reporter - @configuration.event_reporter - end - - def sleep_time_seconds - @configuration.sleep_time_seconds + configuration.handler end def log_labels(extra = {}) diff --git a/lib/gitlab/memory/watchdog/configuration.rb b/lib/gitlab/memory/watchdog/configuration.rb index 5e2154af434..5c459220be8 100644 --- a/lib/gitlab/memory/watchdog/configuration.rb +++ b/lib/gitlab/memory/watchdog/configuration.rb @@ -19,6 +19,10 @@ module Gitlab end end + def empty? + @monitors.empty? + end + private def build_monitor_state(monitor_class, *args, max_strikes:, monitor_name: nil, **kwargs, &block) diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb index 8fbfe7af713..3ead1cae8df 100644 --- a/lib/system_check/helpers.rb +++ b/lib/system_check/helpers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module SystemCheck + # Helpers used inside a SystemCheck instance to standardize output responses module Helpers include ::Gitlab::TaskHelpers diff --git a/lib/system_check/multi_check_helpers.rb b/lib/system_check/multi_check_helpers.rb new file mode 100644 index 00000000000..1b06864a63e --- /dev/null +++ b/lib/system_check/multi_check_helpers.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module SystemCheck + # Helpers used inside a SystemCheck instance to standardize output responses + # when using a multi_check version + module MultiCheckHelpers + def print_skipped(reason) + $stdout.puts 'skipped'.color(:magenta) + + $stdout.puts ' Reason:'.color(:blue) + $stdout.puts " #{reason}" + end + + def print_warning(reason) + $stdout.puts 'warning'.color(:magenta) + + $stdout.puts ' Reason:'.color(:blue) + $stdout.puts " #{reason}" + end + + def print_failure(reason) + $stdout.puts 'no'.color(:red) + + $stdout.puts ' Reason:'.color(:blue) + $stdout.puts " #{reason}" + end + + def print_pass + $stdout.puts self.class.check_pass.color(:green) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 27b97be0fd3..df806938140 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17655,12 +17655,6 @@ msgstr "" msgid "Format: %{dateFormat}" msgstr "" -msgid "Found errors in your %{gitlab_ci_yml}:" -msgstr "" - -msgid "Found errors in your .gitlab-ci.yml:" -msgstr "" - msgid "Framework successfully deleted" msgstr "" @@ -43941,6 +43935,9 @@ msgstr "" msgid "Unable to create link to vulnerability" msgstr "" +msgid "Unable to create pipeline" +msgstr "" + msgid "Unable to fetch branch list for this project." msgstr "" @@ -47314,9 +47311,6 @@ msgstr "" msgid "You can also star a label to make it a priority label." msgstr "" -msgid "You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}" -msgstr "" - msgid "You can also upload existing files from your computer using the instructions below." msgstr "" @@ -47446,9 +47440,6 @@ msgstr "" msgid "You can specify notification level per group or per project." msgstr "" -msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}." -msgstr "" - msgid "You can view the source or %{linkStart}%{cloneIcon} clone the repository%{linkEnd}" msgstr "" diff --git a/package.json b/package.json index 4b1cbb96fe2..e7e4e6390e8 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@cubejs-client/core": "^0.31.0", "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", - "@gitlab/fonts": "^1.0.0", + "@gitlab/fonts": "^1.0.1", "@gitlab/svgs": "3.13.0", "@gitlab/ui": "52.3.0", "@gitlab/visual-review-tools": "1.7.3", diff --git a/spec/features/markdown/observability_spec.rb b/spec/features/markdown/observability_spec.rb new file mode 100644 index 00000000000..0c7d8cc006b --- /dev/null +++ b/spec/features/markdown/observability_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Observability rendering', :js do + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :repository, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:observable_url) { "https://observe.gitlab.com/" } + + let_it_be(:expected) do + %(<iframe src="#{observable_url}?theme=light&kiosk" frameborder="0") + end + + before do + project.add_maintainer(user) + sign_in(user) + end + + context 'when embedding in an issue' do + let(:issue) do + create(:issue, project: project, description: observable_url) + end + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'renders iframe in description' do + page.within('.description') do + expect(page.html).to include(expected) + end + end + + it 'renders iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect(page.html).to include(expected) + end + end + end + + context 'when embedding in an MR' do + let(:merge_request) do + create(:merge_request, source_project: project, target_project: project, description: observable_url) + end + + before do + visit merge_request_path(merge_request) + wait_for_requests + end + + it 'renders iframe in description' do + page.within('.description') do + expect(page.html).to include(expected) + end + end + + it 'renders iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect(page.html).to include(expected) + end + end + end +end diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js new file mode 100644 index 00000000000..c87d11742dc --- /dev/null +++ b/spec/frontend/behaviors/markdown/render_observability_spec.js @@ -0,0 +1,38 @@ +import renderObservability from '~/behaviors/markdown/render_observability'; +import * as ColorUtils from '~/lib/utils/color_utils'; + +describe('Observability iframe renderer', () => { + const findObservabilityIframes = (theme = 'light') => + document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`); + + const renderEmbeddedObservability = () => { + renderObservability([...document.querySelectorAll('.js-render-observability')]); + jest.runAllTimers(); + }; + + beforeEach(() => { + document.body.dataset.page = ''; + document.body.innerHTML = ''; + }); + + it('renders an observability iframe', () => { + document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`; + + expect(findObservabilityIframes()).toHaveLength(0); + + renderEmbeddedObservability(); + + expect(findObservabilityIframes()).toHaveLength(1); + }); + + it('renders iframe with dark param when GL has dark theme', () => { + document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`; + jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true); + + expect(findObservabilityIframes('dark')).toHaveLength(0); + + renderEmbeddedObservability(); + + expect(findObservabilityIframes('dark')).toHaveLength(1); + }); +}); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 545924c9c11..d82b97561f0 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -185,7 +185,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:'); + expect(wrapper.text()).toContain('Unable to create pipeline'); expect(wrapper.text()).toContain(yamlError); }); }); diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js index b896437ecb2..31e097cfa7b 100644 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ b/spec/frontend/import_entities/components/group_dropdown_spec.js @@ -1,16 +1,61 @@ import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import GroupDropdown from '~/import_entities/components/group_dropdown.vue'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; + +Vue.use(VueApollo); + +const makeGroupMock = (fullPath) => ({ + id: `gid://gitlab/Group/${fullPath}`, + fullPath, + name: fullPath, + visibility: 'public', + webUrl: `http://gdk.test:3000/groups/${fullPath}`, + __typename: 'Group', +}); + +const AVAILABLE_NAMESPACES = [ + makeGroupMock('match1'), + makeGroupMock('unrelated'), + makeGroupMock('match2'), +]; + +const SEARCH_NAMESPACES_MOCK = Promise.resolve({ + data: { + currentUser: { + id: 'gid://gitlab/User/1', + groups: { + nodes: AVAILABLE_NAMESPACES, + __typename: 'GroupConnection', + }, + namespace: { + id: 'gid://gitlab/Namespaces::UserNamespace/1', + fullPath: 'root', + __typename: 'Namespace', + }, + __typename: 'UserCore', + }, + }, +}); describe('Import entities group dropdown component', () => { let wrapper; let namespacesTracker; const createComponent = (propsData) => { + const apolloProvider = createMockApollo([ + [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK], + ]); + namespacesTracker = jest.fn(); wrapper = shallowMount(GroupDropdown, { + apolloProvider, scopedSlots: { default: namespacesTracker, }, @@ -23,33 +68,30 @@ describe('Import entities group dropdown component', () => { wrapper.destroy(); }); - it('passes namespaces from props to default slot', () => { - const namespaces = [ - { id: 1, fullPath: 'ns1' }, - { id: 2, fullPath: 'ns2' }, - ]; - createComponent({ namespaces }); + it('passes namespaces from graphql query to default slot', async () => { + createComponent(); + jest.advanceTimersByTime(DEBOUNCE_DELAY); + await nextTick(); + await waitForPromises(); + await nextTick(); - expect(namespacesTracker).toHaveBeenCalledWith({ namespaces }); + expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES }); }); it('filters namespaces based on user input', async () => { - const namespaces = [ - { id: 1, fullPath: 'match1' }, - { id: 2, fullPath: 'some unrelated' }, - { id: 3, fullPath: 'match2' }, - ]; - createComponent({ namespaces }); + createComponent(); namespacesTracker.mockReset(); wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match'); - + jest.advanceTimersByTime(DEBOUNCE_DELAY); + await nextTick(); + await waitForPromises(); await nextTick(); expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: [ - { id: 1, fullPath: 'match1' }, - { id: 3, fullPath: 'match2' }, + expect.objectContaining({ fullPath: 'match1' }), + expect.objectContaining({ fullPath: 'match2' }), ], }); }); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 61f860688dc..f7a97f22d44 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -15,8 +15,13 @@ import ImportTable from '~/import_entities/import_groups/components/import_table import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; -import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures'; +import { + AVAILABLE_NAMESPACES, + availableNamespacesFixture, + generateFakeEntry, +} from '../graphql/fixtures'; jest.mock('~/flash'); jest.mock('~/import_entities/import_groups/services/status_poller'); @@ -60,15 +65,22 @@ describe('import table', () => { wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true); const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => { - apolloProvider = createMockApollo([], { - Query: { - availableNamespaces: () => availableNamespacesFixture, - bulkImportSourceGroups, - }, - Mutation: { - importGroups, + apolloProvider = createMockApollo( + [ + [ + searchNamespacesWhereUserCanCreateProjectsQuery, + () => Promise.resolve(availableNamespacesFixture), + ], + ], + { + Query: { + bulkImportSourceGroups, + }, + Mutation: { + importGroups, + }, }, - }); + ); wrapper = mount(ImportTable, { propsData: { @@ -173,7 +185,7 @@ describe('import table', () => { }); it('respects default namespace if provided', async () => { - const targetNamespace = availableNamespacesFixture[1]; + const targetNamespace = AVAILABLE_NAMESPACES[1]; createComponent({ bulkImportSourceGroups: () => ({ @@ -227,7 +239,7 @@ describe('import table', () => { { newName: FAKE_GROUP.lastImportTarget.newName, sourceGroupId: FAKE_GROUP.id, - targetNamespace: availableNamespacesFixture[0].fullPath, + targetNamespace: AVAILABLE_NAMESPACES[0].fullPath, }, ], }, @@ -519,12 +531,12 @@ describe('import table', () => { variables: { importRequests: [ { - targetNamespace: availableNamespacesFixture[0].fullPath, + targetNamespace: AVAILABLE_NAMESPACES[0].fullPath, newName: NEW_GROUPS[0].lastImportTarget.newName, sourceGroupId: NEW_GROUPS[0].id, }, { - targetNamespace: availableNamespacesFixture[0].fullPath, + targetNamespace: AVAILABLE_NAMESPACES[0].fullPath, newName: NEW_GROUPS[1].lastImportTarget.newName, sourceGroupId: NEW_GROUPS[1].id, }, diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index 18dc1217fec..d5286e71c44 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -1,9 +1,22 @@ import { GlDropdownItem, GlFormInput } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue'; import { STATUSES } from '~/import_entities/constants'; import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; -import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql'; + +import { + generateFakeEntry, + availableNamespacesFixture, + AVAILABLE_NAMESPACES, +} from '../graphql/fixtures'; + +Vue.use(VueApollo); const generateFakeTableEntry = ({ flags = {}, ...config }) => { const entry = generateFakeEntry(config); @@ -11,7 +24,7 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => { return { ...entry, importTarget: { - targetNamespace: availableNamespacesFixture[0], + targetNamespace: AVAILABLE_NAMESPACES[0], newName: entry.lastImportTarget.newName, }, flags, @@ -20,16 +33,24 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => { describe('import target cell', () => { let wrapper; + let apolloProvider; let group; const findNameInput = () => wrapper.findComponent(GlFormInput); const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown); const createComponent = (props) => { + apolloProvider = createMockApollo([ + [ + searchNamespacesWhereUserCanCreateProjectsQuery, + () => Promise.resolve(availableNamespacesFixture), + ], + ]); + wrapper = shallowMount(ImportTargetCell, { + apolloProvider, stubs: { ImportGroupDropdown }, propsData: { - availableNamespaces: availableNamespacesFixture, groupPathRegex: /.*/, ...props, }, @@ -42,9 +63,12 @@ describe('import target cell', () => { }); describe('events', () => { - beforeEach(() => { + beforeEach(async () => { group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE }); createComponent({ group }); + await nextTick(); + jest.advanceTimersByTime(DEBOUNCE_DELAY); + await nextTick(); }); it('emits update-new-name when input value is changed', () => { @@ -59,7 +83,9 @@ describe('import target cell', () => { dropdownItem.vm.$emit('click'); expect(wrapper.emitted('update-target-namespace')).toBeDefined(); - expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]); + expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual( + AVAILABLE_NAMESPACES[1], + ); }); }); @@ -94,18 +120,20 @@ describe('import target cell', () => { expect(items).toHaveLength(1); }); - it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => { + it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => { createComponent({ group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }), - availableNamespaces: availableNamespacesFixture, }); + jest.advanceTimersByTime(DEBOUNCE_DELAY); + await waitForPromises(); + await nextTick(); const [firstItem, ...rest] = findNamespaceDropdown() .findAllComponents(GlDropdownItem) .wrappers.map((w) => w.text()); expect(firstItem).toBe('No parent'); - expect(rest).toHaveLength(availableNamespacesFixture.length); + expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length); }); describe('when entity is not available for import', () => { diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index 52c868e5356..adc4ebcffb8 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -10,12 +10,11 @@ import { import { LocalStorageCache } from '~/import_entities/import_groups/graphql/services/local_storage_cache'; import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql'; -import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; -import { statusEndpointFixture, availableNamespacesFixture } from './fixtures'; +import { statusEndpointFixture } from './fixtures'; jest.mock('~/flash'); jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({ @@ -28,7 +27,6 @@ jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache' const FAKE_ENDPOINTS = { status: '/fake_status_url', - availableNamespaces: '/fake_available_namespaces', createBulkImport: '/fake_create_bulk_import', jobs: '/fake_jobs', }; @@ -55,14 +53,6 @@ describe('Bulk import resolvers', () => { client = createClient(); axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture); - axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply( - httpStatus.OK, - availableNamespacesFixture.map((ns) => ({ - id: ns.id, - full_path: ns.fullPath, - })), - ); - client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => { results = data.bulkImportSourceGroups.nodes; }); @@ -75,22 +65,6 @@ describe('Bulk import resolvers', () => { }); describe('queries', () => { - describe('availableNamespaces', () => { - let namespacesResults; - beforeEach(async () => { - const response = await client.query({ query: availableNamespacesQuery }); - namespacesResults = response.data.availableNamespaces; - }); - - it('mirrors REST endpoint response fields', () => { - const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path }); - - expect(namespacesResults.map(extractRelevantFields)).toStrictEqual( - availableNamespacesFixture.map(extractRelevantFields), - ); - }); - }); - describe('bulkImportSourceGroups', () => { it('respects cached import state when provided by group manager', async () => { const [localStorageCache] = LocalStorageCache.mock.instances; diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js index 938020e03f0..7530e9fc348 100644 --- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js +++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js @@ -59,9 +59,36 @@ export const statusEndpointFixture = { }, }; -export const availableNamespacesFixture = Object.freeze([ - { id: 24, fullPath: 'Commit451' }, - { id: 22, fullPath: 'gitlab-org' }, - { id: 23, fullPath: 'gnuwget' }, - { id: 25, fullPath: 'jashkenas' }, -]); +const makeGroupMock = ({ id, fullPath }) => ({ + id, + fullPath, + name: fullPath, + visibility: 'public', + webUrl: `http://gdk.test:3000/groups/${fullPath}`, + __typename: 'Group', +}); + +export const AVAILABLE_NAMESPACES = [ + makeGroupMock({ id: 24, fullPath: 'Commit451' }), + makeGroupMock({ id: 22, fullPath: 'gitlab-org' }), + makeGroupMock({ id: 23, fullPath: 'gnuwget' }), + makeGroupMock({ id: 25, fullPath: 'jashkenas' }), +]; + +export const availableNamespacesFixture = { + data: { + currentUser: { + id: 'gid://gitlab/User/1', + groups: { + nodes: AVAILABLE_NAMESPACES, + __typename: 'GroupConnection', + }, + namespace: { + id: 'gid://gitlab/Namespaces::UserNamespace/1', + fullPath: 'root', + __typename: 'Namespace', + }, + __typename: 'UserCore', + }, + }, +}; diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js index 53807167fe8..8b695f188cc 100644 --- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js +++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js @@ -95,12 +95,6 @@ describe('ImportProjectsTable', () => { expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); - it('renders a loading icon while namespaces are loading', () => { - createComponent({ state: { isLoadingNamespaces: true } }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - it('renders a table with provider repos', () => { const repositories = [ { importSource: { id: 1 }, importedProject: null }, diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index 40934e90b78..f759e0c029a 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -16,7 +16,6 @@ describe('ProviderRepoTableRow', () => { newName: 'newName', }; - const availableNamespaces = ['test']; const userNamespace = 'root'; function initStore(initialState) { @@ -44,7 +43,7 @@ describe('ProviderRepoTableRow', () => { wrapper = shallowMount(ProviderRepoTableRow, { store, - propsData: { availableNamespaces, userNamespace, optionalStages: {}, ...props }, + propsData: { userNamespace, optionalStages: {}, ...props }, }); } @@ -78,9 +77,7 @@ describe('ProviderRepoTableRow', () => { }); it('renders a group namespace select', () => { - expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe( - availableNamespaces, - ); + expect(wrapper.findComponent(ImportGroupDropdown).exists()).toBe(true); }); it('renders import button', () => { diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index e154863f339..945ba64e5e2 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -13,9 +13,6 @@ import { RECEIVE_IMPORT_SUCCESS, RECEIVE_IMPORT_ERROR, RECEIVE_JOBS_SUCCESS, - REQUEST_NAMESPACES, - RECEIVE_NAMESPACES_SUCCESS, - RECEIVE_NAMESPACES_ERROR, SET_PAGE, SET_FILTER, } from '~/import_entities/import_projects/store/mutation_types'; @@ -30,7 +27,6 @@ const endpoints = { reposPath: MOCK_ENDPOINT, importPath: MOCK_ENDPOINT, jobsPath: MOCK_ENDPOINT, - namespacesPath: MOCK_ENDPOINT, }; const { @@ -40,7 +36,6 @@ const { fetchRepos, fetchImport, fetchJobs, - fetchNamespaces, setFilter, } = actionsFactory({ endpoints, @@ -319,51 +314,6 @@ describe('import_projects store actions', () => { }); }); - describe('fetchNamespaces', () => { - let mock; - const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }]; - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => mock.restore()); - - it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => { - mock.onGet(MOCK_ENDPOINT).reply(200, namespaces); - - await testAction( - fetchNamespaces, - null, - localState, - [ - { type: REQUEST_NAMESPACES }, - { - type: RECEIVE_NAMESPACES_SUCCESS, - payload: convertObjectPropsToCamelCase(namespaces, { deep: true }), - }, - ], - [], - ); - }); - - it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => { - mock.onGet(MOCK_ENDPOINT).reply(500); - - await testAction( - fetchNamespaces, - null, - localState, - [{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }], - [], - ); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'Requesting namespaces failed', - }); - }); - }); - describe('importAll', () => { it('dispatches multiple fetchImport actions', async () => { const OPTIONAL_STAGES = { stage1: true, stage2: false }; diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js index 110b692b222..fced5670f25 100644 --- a/spec/frontend/import_entities/import_projects/store/getters_spec.js +++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js @@ -1,6 +1,5 @@ import { STATUSES } from '~/import_entities/constants'; import { - isLoading, isImportingAnyRepo, hasIncompatibleRepos, hasImportableRepos, @@ -31,24 +30,6 @@ describe('import_projects store getters', () => { }); it.each` - isLoadingRepos | isLoadingNamespaces | isLoadingValue - ${false} | ${false} | ${false} - ${true} | ${false} | ${true} - ${false} | ${true} | ${true} - ${true} | ${true} | ${true} - `( - 'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces', - ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => { - Object.assign(localState, { - isLoadingRepos, - isLoadingNamespaces, - }); - - expect(isLoading(localState)).toBe(isLoadingValue); - }, - ); - - it.each` importStatus | value ${STATUSES.NONE} | ${false} ${STATUSES.SCHEDULING} | ${true} diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js index 77fae951300..b8970916420 100644 --- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js @@ -263,43 +263,6 @@ describe('import_projects store mutations', () => { }); }); - describe(`${types.REQUEST_NAMESPACES}`, () => { - it('sets namespaces loading flag to true', () => { - state = {}; - - mutations[types.REQUEST_NAMESPACES](state); - - expect(state.isLoadingNamespaces).toBe(true); - }); - }); - - describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => { - const response = [{ fullPath: 'some/path' }]; - - beforeEach(() => { - state = {}; - mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response); - }); - - it('stores namespaces to state', () => { - expect(state.namespaces).toStrictEqual(response); - }); - - it('sets namespaces loading flag to false', () => { - expect(state.isLoadingNamespaces).toBe(false); - }); - }); - - describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => { - it('sets namespaces loading flag to false', () => { - state = {}; - - mutations[types.RECEIVE_NAMESPACES_ERROR](state); - - expect(state.isLoadingNamespaces).toBe(false); - }); - }); - describe(`${types.SET_IMPORT_TARGET}`, () => { const PROJECT = { id: 2, diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js index 96b8daa22d8..3f40772f7fc 100644 --- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js +++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js @@ -1,5 +1,6 @@ import { GlEmptyState } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; +import AxiosMockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { cloneDeep } from 'lodash'; @@ -8,9 +9,12 @@ import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_st import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { + filteredTokens, + locationSearch, setSortPreferenceMutationResponse, setSortPreferenceMutationResponseWithErrors, } from 'jest/issues/list/mock_data'; @@ -18,7 +22,12 @@ import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_a import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants'; import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql'; import { getSortKey, getSortOptions } from '~/issues/list/utils'; +import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; +import { + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, +} from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { IssuableStates } from '~/vue_shared/issuable/list/constants'; import { emptyIssuesQueryResponse, issuesQueryResponse } from '../mock_data'; @@ -27,6 +36,7 @@ jest.mock('@sentry/browser'); jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() })); describe('IssuesDashboardApp component', () => { + let axiosMock; let wrapper; Vue.use(VueApollo); @@ -78,8 +88,18 @@ describe('IssuesDashboardApp component', () => { }); }; + beforeEach(() => { + setWindowLocation(TEST_HOST); + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.reset(); + }); + it('renders IssuableList component', async () => { mountComponent(); + jest.runOnlyPendingTimers(); await waitForPromises(); expect(findIssuableList().props()).toMatchObject({ @@ -124,6 +144,7 @@ describe('IssuesDashboardApp component', () => { it('renders issue time information', async () => { mountComponent(); + jest.runOnlyPendingTimers(); await waitForPromises(); expect(findIssueCardTimeInfo().exists()).toBe(true); @@ -131,6 +152,7 @@ describe('IssuesDashboardApp component', () => { it('renders issue statistics', async () => { mountComponent(); + jest.runOnlyPendingTimers(); await waitForPromises(); expect(findIssueCardStatistics().exists()).toBe(true); @@ -147,6 +169,15 @@ describe('IssuesDashboardApp component', () => { }); describe('initial url params', () => { + describe('search', () => { + it('is set from the url params', () => { + setWindowLocation(locationSearch); + mountComponent(); + + expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' }); + }); + }); + describe('sort', () => { describe('when initial sort value uses old enum values', () => { const oldEnumSortValues = Object.values(urlSortParams); @@ -189,11 +220,21 @@ describe('IssuesDashboardApp component', () => { expect(findIssuableList().props('currentTab')).toBe(initialState); }); }); + + describe('filter tokens', () => { + it('is set from the url params', () => { + setWindowLocation(locationSearch); + mountComponent(); + + expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens); + }); + }); }); describe('when there is an error fetching issues', () => { beforeEach(() => { mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) }); + jest.runOnlyPendingTimers(); return waitForPromises(); }); @@ -210,6 +251,40 @@ describe('IssuesDashboardApp component', () => { }); }); + describe('tokens', () => { + const mockCurrentUser = { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: 'avatar/url', + }; + const originalGon = window.gon; + + beforeEach(() => { + window.gon = { + ...originalGon, + current_user_id: mockCurrentUser.id, + current_user_fullname: mockCurrentUser.name, + current_username: mockCurrentUser.username, + current_user_avatar_url: mockCurrentUser.avatar_url, + }; + mountComponent(); + }); + + afterEach(() => { + window.gon = originalGon; + }); + + it('renders all tokens alphabetically', () => { + const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }]; + + expect(findIssuableList().props('searchTokens')).toMatchObject([ + { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers }, + { type: TOKEN_TYPE_AUTHOR, preloadedUsers }, + ]); + }); + }); + describe('events', () => { describe('when "click-tab" event is emitted by IssuableList', () => { beforeEach(() => { diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js index dbdff899bac..911bb8878da 100644 --- a/spec/frontend/terraform/components/init_command_modal_spec.js +++ b/spec/frontend/terraform/components/init_command_modal_spec.js @@ -7,12 +7,13 @@ const accessTokensPath = '/path/to/access-tokens-page'; const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1'; const username = 'username'; const modalId = 'fake-modal-id'; -const stateName = 'production'; +const stateName = 'aws/eu-central-1'; +const stateNameEncoded = encodeURIComponent(stateName); const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN> terraform init \\ - -backend-config="address=${terraformApiUrl}/${stateName}" \\ - -backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\ - -backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\ + -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\ + -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\ + -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\ -backend-config="username=${username}" \\ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\ -backend-config="lock_method=POST" \\ @@ -61,9 +62,15 @@ describe('InitCommandModal', () => { expect(findLink().attributes('href')).toBe(accessTokensPath); }); - it('renders the init command with the username and state name prepopulated', () => { - expect(findInitCommand().text()).toContain(username); - expect(findInitCommand().text()).toContain(stateName); + describe('init command', () => { + it('includes correct address', () => { + expect(findInitCommand().text()).toContain( + `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`, + ); + }); + it('includes correct username', () => { + expect(findInitCommand().text()).toContain(`-backend-config="username=${username}"`); + }); }); it('renders the copyToClipboard button', () => { diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js index 097cab4db06..a86fbc1500e 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js @@ -8,9 +8,9 @@ import extensionsContainer from '~/vue_merge_request_widget/components/extension import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality'; import httpStatusCodes from '~/lib/utils/http_status'; +import { i18n } from '~/vue_merge_request_widget/extensions/code_quality/constants'; import { codeQualityResponseNewErrors, - codeQualityResponseResolvedErrors, codeQualityResponseResolvedAndNewErrors, codeQualityResponseNoErrors, } from './mock_data'; @@ -59,46 +59,55 @@ describe('Code Quality extension', () => { createComponent(); - expect(wrapper.text()).toBe('Code Quality test metrics results are being parsed'); + expect(wrapper.text()).toBe(i18n.loading); }); - it('displays failed loading text', async () => { - mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR); - + it('with a 204 response, continues to display loading state', async () => { + mockApi(httpStatusCodes.NO_CONTENT, ''); createComponent(); await waitForPromises(); - expect(wrapper.text()).toBe('Code Quality failed loading results'); + + expect(wrapper.text()).toBe(i18n.loading); }); - it('displays quality degradation', async () => { - mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors); + it('displays failed loading text', async () => { + mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR); createComponent(); await waitForPromises(); - - expect(wrapper.text()).toBe('Code Quality degraded on 2 points.'); + expect(wrapper.text()).toBe(i18n.error); }); - it('displays quality improvement', async () => { - mockApi(httpStatusCodes.OK, codeQualityResponseResolvedErrors); + it('displays correct single Report', async () => { + mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors); createComponent(); await waitForPromises(); - expect(wrapper.text()).toBe('Code Quality improved on 2 points.'); + expect(wrapper.text()).toBe( + i18n.degradedCopy(i18n.singularReport(codeQualityResponseNewErrors.new_errors)), + ); }); it('displays quality improvement and degradation', async () => { mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors); createComponent(); - await waitForPromises(); - expect(wrapper.text()).toBe('Code Quality improved on 1 point and degraded on 1 point.'); + // replacing strong tags because they will not be found in the rendered text + expect(wrapper.text()).toBe( + i18n + .improvementAndDegradationCopy( + i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.resolved_errors), + i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.new_errors), + ) + .replace(/%{strong_start}/g, '') + .replace(/%{strong_end}/g, ''), + ); }); it('displays no detected errors', async () => { @@ -108,7 +117,7 @@ describe('Code Quality extension', () => { await waitForPromises(); - expect(wrapper.text()).toBe('No changes to Code Quality.'); + expect(wrapper.text()).toBe(i18n.noChanges); }); }); @@ -145,7 +154,7 @@ describe('Code Quality extension', () => { it('adds fixed indicator (badge) when error is resolved', () => { expect(findAllExtensionListItems().at(1).findComponent(GlBadge).exists()).toBe(true); - expect(findAllExtensionListItems().at(1).findComponent(GlBadge).text()).toEqual('Fixed'); + expect(findAllExtensionListItems().at(1).findComponent(GlBadge).text()).toEqual(i18n.fixed); }); it('should not add fixed indicator (badge) when error is new', () => { diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js index f5ad0ce7377..2e8e70f25db 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js @@ -23,31 +23,6 @@ export const codeQualityResponseNewErrors = { }, }; -export const codeQualityResponseResolvedErrors = { - status: 'failed', - new_errors: [], - resolved_errors: [ - { - description: "Parsing error: 'return' outside of function", - severity: 'minor', - file_path: 'index.js', - line: 12, - }, - { - description: 'TODO found', - severity: 'minor', - file_path: '.gitlab-ci.yml', - line: 73, - }, - ], - existing_errors: [], - summary: { - total: 2, - resolved: 2, - errored: 0, - }, -}; - export const codeQualityResponseResolvedAndNewErrors = { status: 'failed', new_errors: [ diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index f5155347a15..9f34d3e947b 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -452,7 +452,7 @@ describe('Web IDE link component', () => { expect(findUserCalloutDismisser().props()).toEqual( expect.objectContaining({ skipQuery: false, - featureName: 'vscode_web_ide', + featureName: 'vscode_web_ide_callout', }), ); }); diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js index 60ba2b55f76..5997de01274 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_spec.js @@ -179,6 +179,18 @@ describe('WorkItemMilestone component', () => { createComponent({ canUpdate: true }); }); + it('calls successSearchQueryHandler with variables when dropdown is opened', async () => { + showDropdown(); + await nextTick(); + + expect(successSearchQueryHandler).toHaveBeenCalledWith({ + first: 20, + fullPath: 'full-path', + state: 'active', + title: '', + }); + }); + it('shows the skeleton loader when the items are being fetched on click', async () => { showDropdown(); await nextTick(); diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb index e4726ad0e6e..f437c3778c6 100644 --- a/spec/graphql/types/permission_types/base_permission_type_spec.rb +++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb @@ -51,4 +51,25 @@ RSpec.describe Types::PermissionTypes::BasePermissionType do expect(test_type).to have_graphql_field(:admin_issue) end end + + describe 'extensions' do + subject(:test_type) do + Class.new(described_class) do + graphql_name 'TestClass' + + permission_field :read_entity_a do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + + ability_field(:read_entity_b) do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + end + end + + it 'has the extension' do + expect(test_type.fields['readEntityA'].extensions).to include(a_kind_of(::Gitlab::Graphql::Limit::FieldCallCount)) + expect(test_type.fields['readEntityB'].extensions).to include(a_kind_of(::Gitlab::Graphql::Limit::FieldCallCount)) + end + end end diff --git a/spec/graphql/types/permission_types/deployment_spec.rb b/spec/graphql/types/permission_types/deployment_spec.rb index 0f065b97550..ccf5798984c 100644 --- a/spec/graphql/types/permission_types/deployment_spec.rb +++ b/spec/graphql/types/permission_types/deployment_spec.rb @@ -4,12 +4,8 @@ require 'spec_helper' RSpec.describe Types::PermissionTypes::Deployment, feature_category: :continuous_delivery do it do - expected_permissions = [ - :update_deployment, :destroy_deployment - ] + expected_permissions = %i[update_deployment destroy_deployment] - expected_permissions.each do |permission| - expect(described_class).to have_graphql_field(permission) - end + expect(described_class).to include_graphql_fields(*expected_permissions) end end diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb new file mode 100644 index 00000000000..341ada6d2b5 --- /dev/null +++ b/spec/lib/banzai/filter/inline_observability_filter_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::InlineObservabilityFilter do + include FilterSpecHelper + + let(:input) { %(<a href="#{url}">example</a>) } + let(:doc) { filter(input) } + + context 'when the document has an external link' do + let(:url) { 'https://foo.com' } + + it 'leaves regular non-observability links unchanged' do + expect(doc.to_s).to eq(input) + end + end + + context 'when the document contains an embeddable observability link' do + let(:url) { 'https://observe.gitlab.com/12345' } + + it 'leaves the original link unchanged' do + expect(doc.at_css('a').to_s).to eq(input) + end + + it 'appends a observability charts placeholder' do + node = doc.at_css('.js-render-observability') + + expect(node).to be_present + expect(node.attribute('data-frame-url').to_s).to eq(url) + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 911b0fa7192..b48a89059bf 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config do +RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_authoring do include StubRequests let_it_be(:user) { create(:user) } @@ -503,7 +503,7 @@ RSpec.describe Gitlab::Ci::Config do expect { config }.to raise_error( described_class::ConfigError, - 'Resolving config took longer than expected' + 'Request timed out when fetching configuration files.' ) end end diff --git a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb index f1405ca7d68..9242344ead2 100644 --- a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb +++ b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb @@ -22,6 +22,10 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do describe '#event_reporter' do context 'when event reporter is not set' do + before do + allow(Gitlab::Metrics).to receive(:counter) + end + it 'defaults to EventReporter' do expect(configuration.event_reporter).to be_an_instance_of(::Gitlab::Memory::Watchdog::EventReporter) end @@ -100,6 +104,16 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do expect(strikes).to eq([false, true]) expect(monitor_names).to eq([monitor_name1, monitor_name2]) end + + it 'monitors are not empty' do + expect(configuration.monitors).not_to be_empty + end + end + + context 'when monitors are not configured' do + it 'monitors are empty' do + expect(configuration.monitors).to be_empty + end end context 'when monitors are configured inline' do diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb index 92aa583a26d..668ea36d420 100644 --- a/spec/lib/gitlab/memory/watchdog_spec.rb +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -49,7 +49,6 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: config.handler = handler config.event_reporter = reporter config.sleep_time_seconds = sleep_time_seconds - config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes end allow(handler).to receive(:call).and_return(true) @@ -75,42 +74,49 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: watchdog.call end - context 'when gitlab_memory_watchdog ops toggle is off' do - before do - stub_feature_flags(gitlab_memory_watchdog: false) - end + context 'when no monitors are configured' do + it 'reports stopped event once with correct reason' do + expect(reporter).to receive(:stopped).once + .with( + memwd_handler_class: handler.class.name, + memwd_sleep_time_s: sleep_time_seconds, + memwd_reason: 'monitors are not configured' + ) - it 'does not trigger any monitor' do - expect(configuration).not_to receive(:monitors) + watchdog.call end end - context 'when process does not exceed threshold' do - it 'does not report violations event' do - expect(reporter).not_to receive(:threshold_violated) - expect(reporter).not_to receive(:strikes_exceeded) - - watchdog.call + context 'when monitors are configured' do + before do + watchdog.configure do |config| + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes + end end - it 'does not execute handler' do - expect(handler).not_to receive(:call) + it 'reports stopped event once' do + expect(reporter).to receive(:stopped).once + .with( + memwd_handler_class: handler.class.name, + memwd_sleep_time_s: sleep_time_seconds + ) watchdog.call end - end - - context 'when process exceeds threshold' do - let(:threshold_violated) { true } - it 'reports threshold violated event' do - expect(reporter).to receive(:threshold_violated).with(name) + context 'when gitlab_memory_watchdog ops toggle is off' do + before do + stub_feature_flags(gitlab_memory_watchdog: false) + end - watchdog.call + it 'does not trigger any monitor' do + expect(configuration).not_to receive(:monitors) + end end - context 'when process does not exceed the allowed number of strikes' do - it 'does not report strikes exceeded event' do + context 'when process does not exceed threshold' do + it 'does not report violations event' do + expect(reporter).not_to receive(:threshold_violated) expect(reporter).not_to receive(:strikes_exceeded) watchdog.call @@ -123,77 +129,94 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: end end - context 'when monitor exceeds the allowed number of strikes' do - let(:max_strikes) { 0 } + context 'when process exceeds threshold' do + let(:threshold_violated) { true } - it 'reports strikes exceeded event' do - expect(reporter).to receive(:strikes_exceeded) - .with( - name, - memwd_handler_class: handler.class.name, - memwd_sleep_time_s: sleep_time_seconds, - memwd_cur_strikes: 1, - memwd_max_strikes: max_strikes, - message: "dummy_text" - ) + it 'reports threshold violated event' do + expect(reporter).to receive(:threshold_violated).with(name) watchdog.call end - it 'executes handler' do - expect(handler).to receive(:call) + context 'when process does not exceed the allowed number of strikes' do + it 'does not report strikes exceeded event' do + expect(reporter).not_to receive(:strikes_exceeded) - watchdog.call - end + watchdog.call + end - it 'schedules a heap dump' do - expect(Gitlab::Memory::Reports::HeapDump).to receive(:enqueue!) + it 'does not execute handler' do + expect(handler).not_to receive(:call) - watchdog.call + watchdog.call + end end - context 'when enforce_memory_watchdog ops toggle is off' do - before do - stub_feature_flags(enforce_memory_watchdog: false) + context 'when monitor exceeds the allowed number of strikes' do + let(:max_strikes) { 0 } + + it 'reports strikes exceeded event' do + expect(reporter).to receive(:strikes_exceeded) + .with( + name, + memwd_handler_class: handler.class.name, + memwd_sleep_time_s: sleep_time_seconds, + memwd_cur_strikes: 1, + memwd_max_strikes: max_strikes, + message: "dummy_text" + ) + + watchdog.call end - it 'always uses the NullHandler' do - expect(handler).not_to receive(:call) - expect(described_class::NullHandler.instance).to receive(:call).and_return(true) + it 'executes handler and stops the watchdog' do + expect(handler).to receive(:call).and_return(true) + expect(reporter).to receive(:stopped).once + .with( + memwd_handler_class: handler.class.name, + memwd_sleep_time_s: sleep_time_seconds, + memwd_reason: 'successfully handled' + ) watchdog.call end - end - context 'when multiple monitors exceeds allowed number of strikes' do - before do - watchdog.configure do |config| - config.handler = handler - config.event_reporter = reporter - config.sleep_time_seconds = sleep_time_seconds - config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes - config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes + it 'schedules a heap dump' do + expect(Gitlab::Memory::Reports::HeapDump).to receive(:enqueue!) + + watchdog.call + end + + context 'when enforce_memory_watchdog ops toggle is off' do + before do + stub_feature_flags(enforce_memory_watchdog: false) + end + + it 'always uses the NullHandler' do + expect(handler).not_to receive(:call) + expect(described_class::NullHandler.instance).to receive(:call).and_return(true) + + watchdog.call end end - it 'only calls the handler once' do - expect(handler).to receive(:call).once.and_return(true) + context 'when multiple monitors exceeds allowed number of strikes' do + before do + watchdog.configure do |config| + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes + end + end + + it 'only calls the handler once' do + expect(handler).to receive(:call).once.and_return(true) - watchdog.call + watchdog.call + end end end end end - - it 'reports stopped event once' do - expect(reporter).to receive(:stopped).once - .with( - memwd_handler_class: handler.class.name, - memwd_sleep_time_s: sleep_time_seconds - ) - - watchdog.call - end end describe '#configure' do diff --git a/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb b/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb new file mode 100644 index 00000000000..3341df2ce51 --- /dev/null +++ b/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UpdateActiveBillableUsersIndex, feature_category: :database do + let(:db) { described_class.new } + let(:table_name) { described_class::TABLE_NAME } + let(:old_index_name) { described_class::OLD_INDEX_NAME } + let(:new_index_name) { described_class::NEW_INDEX_NAME } + let(:old_filter_condition) { "(user_type <> ALL ('{2,6,1,3,7,8}'::smallint[])))" } + let(:new_filter_condition) { "(user_type <> ALL ('{1,2,3,4,5,6,7,8,9,11}'::smallint[])))" } + + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(subject.index_exists_by_name?(table_name, new_index_name)).to be_falsy + expect(subject.index_exists_by_name?(table_name, old_index_name)).to be_truthy + expect(db.connection.indexes(table_name).find do |i| + i.name == old_index_name + end.where).to include(old_filter_condition) + } + + migration.after -> { + expect(subject.index_exists_by_name?(table_name, old_index_name)).to be_falsy + expect(subject.index_exists_by_name?(table_name, new_index_name)).to be_truthy + expect(db.connection.indexes(table_name).find do |i| + i.name == new_index_name + end.where).to include(new_filter_condition) + } + end + end +end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 40307cc9c2f..9b70f7c2839 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -264,6 +264,17 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d end end + describe '#worker_cron' do + before do + allow(Settings).to receive(:cron_jobs) + .and_return({ pipeline_schedule_worker: { cron: "* 1 2 3 4" } }.with_indifferent_access) + end + + it "returns cron expression set in Settings" do + expect(subject.worker_cron_expression).to eq("* 1 2 3 4") + end + end + context 'loose foreign key on ci_pipeline_schedules.project_id' do it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index b175ccf4c6f..c593b5aba07 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -134,7 +134,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do runner_data = graphql_data_at(:runner) expect(runner_data).not_to be_nil - expect(runner_data).to match a_graphql_entity_for(runner, :tag_list, admin_url: nil) + expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil) + expect(runner_data['tagList']).to match_array runner.tag_list end end diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 3d25919b760..fd34345d814 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -113,6 +113,17 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it_behaves_like 'can access terraform state' + end + context 'for a project that does not exist' do let(:project_id) { '0000' } @@ -264,6 +275,21 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu expect(Gitlab::Json.parse(response.body)).to be_empty end end + + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:non_existing_state_name) { 'state-name-with-dot.tfstate' } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'strips characters after the dot' do + expect { request }.to change { Terraform::State.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(Terraform::State.last.name).to eq('state-name-with-dot') + end + end end context 'without body' do @@ -347,6 +373,18 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu it_behaves_like 'endpoint with unique user tracking' + shared_examples 'schedules the state for deletion' do + it 'returns empty body' do + expect(Terraform::States::TriggerDestroyService).to receive(:new).and_return(deletion_service) + expect(deletion_service).to receive(:execute).once + + request + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)).to be_empty + end + end + context 'with maintainer permissions' do let(:current_user) { maintainer } let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) } @@ -355,15 +393,19 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu with_them do let(:state_name) { given_state_name } - it 'schedules the state for deletion and returns empty body' do - expect(Terraform::States::TriggerDestroyService).to receive(:new).and_return(deletion_service) - expect(deletion_service).to receive(:execute).once + it_behaves_like 'schedules the state for deletion' + end - request + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_name_with_dot) { "#{state_name}.tfstate" } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" } - expect(response).to have_gitlab_http_status(:ok) - expect(Gitlab::Json.parse(response.body)).to be_empty + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) end + + it_behaves_like 'schedules the state for deletion' end context 'with invalid state name' do @@ -390,7 +432,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end - describe 'PUT /projects/:id/terraform/state/:name/lock' do + describe 'POST /projects/:id/terraform/state/:name/lock' do let(:params) do { ID: '123-456', @@ -408,17 +450,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu it_behaves_like 'endpoint with unique user tracking' it_behaves_like 'cannot access a state that is scheduled for deletion' - where(given_state_name: %w[test-state test.state test%2Ffoo]) - with_them do - let(:state_name) { given_state_name } - - it 'locks the terraform state' do - request - - expect(response).to have_gitlab_http_status(:ok) - end - end - context 'with invalid state name' do let(:state_name) { 'foo/bar' } @@ -450,6 +481,47 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu expect(response).to have_gitlab_http_status(:forbidden) end end + + where(given_state_name: %w[test-state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } + + it 'locks the terraform state' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with a dot in the state name' do + let(:state_name) { 'test.state' } + + context 'with allow_dots_on_tf_state_names ff enabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: true) + end + + let(:state_name) { 'test.state' } + + it 'locks the terraform state' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end describe 'DELETE /projects/:id/terraform/state/:name/lock' do @@ -466,8 +538,9 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end before do - state.lock_xid = '123-456' + state.lock_xid = '123.456' state.save! + stub_feature_flags(allow_dots_on_tf_state_names: true) end subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } @@ -485,7 +558,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu let(:state_name) { given_state_name } context 'with the correct lock id' do - let(:lock_id) { '123-456' } + let(:lock_id) { '123.456' } it 'removes the terraform state lock' do request @@ -494,6 +567,23 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + context 'with dots in the state name' do + let(:lock_id) { '123.456' } + let(:state_name) { 'test.state' } + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + context 'with no lock id (force-unlock)' do let(:params) { {} } @@ -506,7 +596,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end context 'with invalid state name' do - let(:lock_id) { '123-456' } + let(:lock_id) { '123.456' } let(:state_name) { 'foo/bar' } it 'returns a 404 error' do @@ -517,7 +607,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end context 'with an incorrect lock id' do - let(:lock_id) { '456-789' } + let(:lock_id) { '456.789' } it 'returns an error' do request @@ -537,7 +627,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end context 'user does not have permission to unlock the state' do - let(:lock_id) { '123-456' } + let(:lock_id) { '123.456' } let(:current_user) { developer } it 'returns an error' do diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index 4b99f74e9c6..b9c7da20d1a 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'projects/pipelines/show' do +RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_authoring do include Devise::Test::ControllerHelpers let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -23,7 +23,7 @@ RSpec.describe 'projects/pipelines/show' do it 'shows errors' do render - expect(rendered).to have_content('Found errors in your .gitlab-ci.yml') + expect(rendered).to have_content('Unable to create pipeline') expect(rendered).to have_content('some errors') end @@ -38,7 +38,7 @@ RSpec.describe 'projects/pipelines/show' do it 'does not show errors' do render - expect(rendered).not_to have_content('Found errors in your .gitlab-ci.yml') + expect(rendered).not_to have_content('Unable to create pipeline') end it 'renders the pipeline tabs' do diff --git a/yarn.lock b/yarn.lock index 368e12f1287..cba318acbbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1117,10 +1117,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/favicon-overlay/-/favicon-overlay-2.0.0.tgz#2f32d0b6a4d5b8ac44e2927083d9ab478a78c984" integrity sha512-GNcORxXJ98LVGzOT9dDYKfbheqH6lNgPDD72lyXRnQIH7CjgGyos8i17aSBPq1f4s3zF3PyedFiAR4YEZbva2Q== -"@gitlab/fonts@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.0.0.tgz#dceabce78519aeb96682f80afa3f03ec0bb05b4f" - integrity sha512-RKbAxFHB3n2x6cAxM9x2hSvOEhJjiXFfKXEFVERinPefCKgMOG6AkWm54sNUfmm3HVEcFMcHwdaRV/DVwVdykg== +"@gitlab/fonts@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.0.1.tgz#5bcdf85d240fb053a7606a4cb79ab1c9b643f148" + integrity sha512-0e2a7x4A9ngEO/SsV/OQZYvuEAfLxKlS5Smn2zg+XI/6WlyFWNjmxDvdD58zav9Pjar1jrNPWMDLtGqV4U+QxQ== "@gitlab/stylelint-config@4.1.0": version "4.1.0" |