diff options
68 files changed, 920 insertions, 265 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 9a13600a86c..478fd01f541 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -22,6 +22,7 @@ variables: DECOMPOSED_DB: "true" GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: "main" + GITLAB_USE_MODEL_LOAD_BALANCING: "true" .rspec-base: extends: .rails-job-base diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue index ed90343777d..e949498c55b 100644 --- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue +++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue @@ -138,7 +138,7 @@ export default { /> </form> <template #modal-footer> - <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button> + <gl-button @click="onCancel">{{ __('Cancel') }}</gl-button> <gl-button :disabled="!canSubmit" category="secondary" diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue index 1eb4832a2a3..63ec40d4ec6 100644 --- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue +++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue @@ -3,7 +3,7 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { GlSingleStat } from '@gitlab/ui/dist/charts'; import createFlash from '~/flash'; import { number } from '~/lib/utils/unit_format'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql'; const defaultPrecision = 0; @@ -52,7 +52,7 @@ export default { mergeRequests: s__('UsageTrends|Merge requests'), pipelines: s__('UsageTrends|Pipelines'), }, - loadCountsError: s__('Could not load usage counts. Please refresh the page to try again.'), + loadCountsError: __('Could not load usage counts. Please refresh the page to try again.'), }, }; </script> diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue index 825807e833e..0303930de5d 100644 --- a/app/assets/javascripts/badges/components/badge_settings.vue +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -2,7 +2,7 @@ import { GlSprintf, GlModal } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; import createFlash from '~/flash'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import Badge from './badge.vue'; import BadgeForm from './badge_form.vue'; import BadgeList from './badge_list.vue'; @@ -25,13 +25,13 @@ export default { ...mapState(['badgeInModal', 'isEditing']), primaryProps() { return { - text: s__('Delete badge'), + text: __('Delete badge'), attributes: [{ category: 'primary' }, { variant: 'danger' }], }; }, cancelProps() { return { - text: s__('Cancel'), + text: __('Cancel'), }; }, }, diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue index a3278f8bde2..e75aa523ed0 100644 --- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue +++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue @@ -1,7 +1,7 @@ <script> import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui'; import Cookies from 'js-cookie'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import Tracking from '~/tracking'; const trackingMixin = Tracking.mixin(); @@ -69,7 +69,7 @@ export default { }, }, i18n: { - modalTitle: s__("That's it, well done!"), + modalTitle: __("That's it, well done!"), pipelinesButton: s__('MR widget|See your pipeline in action'), mergeRequestButton: s__('MR widget|Back to the Merge request'), bodyMessage: s__( diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue index b0b787ac3f7..98db620e3ab 100644 --- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -159,7 +159,7 @@ export default { ) }}</span> <template #modal-footer> - <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button> + <gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button> <template v-if="confirmCleanup"> <gl-button :disabled="!canSubmit" diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index 0bade1fc281..ffa28f516e6 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -91,7 +91,7 @@ export const I18N_INSTALL_AGENT_MODAL = { ), basicInstallTitle: s__('ClusterAgents|Recommended installation method'), - basicInstallBody: s__( + basicInstallBody: __( `Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`, ), @@ -100,7 +100,7 @@ export const I18N_INSTALL_AGENT_MODAL = { 'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.', ), - registrationErrorTitle: s__('Failed to register Agent'), + registrationErrorTitle: __('Failed to register Agent'), unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'), }; diff --git a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue index e026391ae22..fdf8b7796bf 100644 --- a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue +++ b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue @@ -62,7 +62,7 @@ export default { </gl-sprintf> {{ s__('DeployTokens|This action cannot be undone.') }} <template #modal-footer> - <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button> + <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button> <gl-button category="primary" variant="danger" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 746646948fe..46726a8fa07 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -50,7 +50,7 @@ export default { mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })], i18n: { ...DIFF_FILE_HEADER, - compareButtonLabel: s__('Compare submodule commit revisions'), + compareButtonLabel: __('Compare submodule commit revisions'), }, props: { discussionPath: { @@ -130,7 +130,7 @@ export default { const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha)); const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha)); return sprintf( - s__('Compare %{oldCommitId}...%{newCommitId}'), + __('Compare %{oldCommitId}...%{newCommitId}'), { oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`, newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`, diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue index 2eb2be351b3..26ec882472b 100644 --- a/app/assets/javascripts/environments/components/delete_environment_modal.vue +++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue @@ -1,6 +1,6 @@ <script> import { GlTooltipDirective, GlModal } from '@gitlab/ui'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import eventHub from '../event_hub'; export default { @@ -27,7 +27,7 @@ export default { }, cancelProps() { return { - text: s__('Cancel'), + text: __('Cancel'), }; }, confirmDeleteMessage() { diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 85cff73cc3e..0f9741784d6 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -6,7 +6,7 @@ import Visibility from 'visibilityjs'; import createFlash from '~/flash'; import Poll from '../../lib/utils/poll'; import { getParameterByName } from '../../lib/utils/url_utility'; -import { s__ } from '../../locale'; +import { s__, __ } from '../../locale'; import tabs from '../../vue_shared/components/navigation_tabs.vue'; import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue'; import container from '../components/container.vue'; @@ -207,13 +207,13 @@ export default { tabs() { return [ { - name: s__('Available'), + name: __('Available'), scope: 'available', count: this.state.availableCounter, isActive: this.scope === 'available', }, { - name: s__('Stopped'), + name: __('Stopped'), scope: 'stopped', count: this.state.stoppedCounter, isActive: this.scope === 'stopped', diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index e8541d3a4c3..1c5a00568eb 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -2,7 +2,7 @@ import { GlModal, GlButton } from '@gitlab/ui'; import { mapActions, mapState, mapGetters } from 'vuex'; import createFlash from '~/flash'; -import { __, sprintf, s__ } from '~/locale'; +import { __, sprintf } from '~/locale'; import { modalTypes } from '../../constants'; import { trimPathComponents, getPathParent } from '../../utils'; @@ -58,7 +58,7 @@ export default { if (this.modalType === modalTypes.rename) { if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) { createFlash({ - message: sprintf(s__('The name "%{name}" is already taken in this directory.'), { + message: sprintf(__('The name "%{name}" is already taken in this directory.'), { name: this.entryName, }), fadeTransition: false, 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 eaa08cb1529..ec6025c84bb 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 @@ -517,7 +517,7 @@ export default { <gl-empty-state v-else-if="!hasGroups" :title="s__('BulkImport|You have no groups to import')" - :description="s__('Check your source instance permissions.')" + :description="__('Check your source instance permissions.')" /> <template v-else> <div diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue index 6e300831e00..799d2bdc9e2 100644 --- a/app/assets/javascripts/issuable/components/issuable_by_email.vue +++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue @@ -166,7 +166,7 @@ export default { </gl-sprintf> </p> <template #modal-footer> - <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button> + <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button> </template> </gl-modal> </div> diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e4173995fc6..d3b58ed3012 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -4,7 +4,7 @@ import Visibility from 'visibilityjs'; import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; import { visitUrl } from '~/lib/utils/url_utility'; -import { __, s__, sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import { IssuableStatus, IssuableStatusText, @@ -250,7 +250,7 @@ export default { return false; }, defaultErrorMessage() { - return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType }); + return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType }); }, isClosed() { return this.issuableStatus === IssuableStatus.Closed; @@ -437,7 +437,7 @@ export default { }) .catch(() => { createFlash({ - message: sprintf(s__('Error deleting %{issuableType}'), { + message: sprintf(__('Error deleting %{issuableType}'), { issuableType: this.issuableType, }), }); diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index d8ca28e33b6..9dc122d426c 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -2,7 +2,7 @@ import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; import createFlash from '~/flash'; -import { s__, sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import TaskList from '../../task_list'; import animateMixin from '../mixins/animate'; @@ -104,7 +104,7 @@ export default { taskListUpdateError() { createFlash({ message: sprintf( - s__( + __( 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.', ), { diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js index 5cccf2e6bce..815f338f1a0 100644 --- a/app/assets/javascripts/issues_list/service_desk_helper.js +++ b/app/assets/javascripts/issues_list/service_desk_helper.js @@ -1,4 +1,4 @@ -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; /** * Generates empty state messages for Service Desk issues list. @@ -25,7 +25,7 @@ export function generateMessages(emptyStateMeta) { const commonDescription = ` <span>${serviceDeskSupportedMessage}</span> - <a href="${serviceDeskHelpPage}">${s__('Learn more.')}</a>`; + <a href="${serviceDeskHelpPage}">${__('Learn more.')}</a>`; return { serviceDeskEnabledAndCanEditProjectSettings: { @@ -60,7 +60,7 @@ export function generateMessages(emptyStateMeta) { 'ServiceDesk|To enable Service Desk on this instance, an instance administrator must first set up incoming email.', ), primaryLink: incomingEmailHelpPage, - primaryText: s__('Learn more.'), + primaryText: __('Learn more.'), }, serviceDeskIsNotEnabled: { title: s__('ServiceDesk|Service Desk is not enabled'), diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 825d9625c8d..9b3cd04c4e2 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -1,55 +1,43 @@ <script> -import { GlAlert, GlButton, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { mapState, mapMutations } from 'vuex'; -import { retrieveAlert, getLocation } from '~/jira_connect/subscriptions/utils'; +import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import { SET_ALERT } from '../store/mutation_types'; import SubscriptionsList from './subscriptions_list.vue'; import AddNamespaceButton from './add_namespace_button.vue'; +import SignInButton from './sign_in_button.vue'; export default { name: 'JiraConnectApp', components: { GlAlert, - GlButton, GlLink, GlSprintf, SubscriptionsList, AddNamespaceButton, + SignInButton, }, inject: { usersPath: { default: '', }, }, - data() { - return { - location: '', - }; - }, computed: { ...mapState(['alert']), - usersPathWithReturnTo() { - if (this.location) { - return `${this.usersPath}?return_to=${this.location}`; - } - - return this.usersPath; - }, shouldShowAlert() { return Boolean(this.alert?.message); }, + userSignedIn() { + return Boolean(!this.usersPath); + }, }, created() { this.setInitialAlert(); - this.setLocation(); }, methods: { ...mapMutations({ setAlert: SET_ALERT, }), - async setLocation() { - this.location = await getLocation(); - }, setInitialAlert() { const { linkUrl, title, message, variant } = retrieveAlert() || {}; this.setAlert({ linkUrl, title, message, variant }); @@ -82,15 +70,7 @@ export default { <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4"> <div class="gl-display-flex gl-justify-content-end"> - <gl-button - v-if="usersPath" - category="primary" - variant="info" - class="gl-align-self-center" - :href="usersPathWithReturnTo" - target="_blank" - >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button - > + <sign-in-button v-if="!userSignedIn" :users-path="usersPath" /> <add-namespace-button v-else /> </div> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue new file mode 100644 index 00000000000..08f15519679 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue @@ -0,0 +1,50 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { getLocation } from '~/jira_connect/subscriptions/utils'; +import { objectToQuery } from '~/lib/utils/url_utility'; + +export default { + components: { + GlButton, + }, + props: { + usersPath: { + type: String, + required: true, + }, + }, + data() { + return { + location: '', + }; + }, + computed: { + usersPathWithReturnTo() { + if (this.location) { + const queryParams = { + return_to: this.location, + }; + + return `${this.usersPath}?${objectToQuery(queryParams)}`; + } + + return this.usersPath; + }, + }, + created() { + this.setLocation(); + }, + methods: { + async setLocation() { + this.location = await getLocation(); + }, + }, +}; +</script> +<template> + <gl-button category="primary" variant="info" :href="usersPathWithReturnTo" target="_blank"> + <slot> + {{ s__('Integrations|Sign in to add namespaces') }} + </slot> + </gl-button> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js index f1262be0174..46a736c991e 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/index.js +++ b/app/assets/javascripts/jira_connect/subscriptions/index.js @@ -11,6 +11,9 @@ import { getLocation, sizeToParent } from './utils'; const store = createStore(); +/** + * Add `return_to` query param to all HAML-defined GitLab sign in links. + */ const updateSignInLinks = async () => { const location = await getLocation(); Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => { diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js index 3c446c21865..7bff2bf3e47 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js @@ -14,33 +14,33 @@ import { s__, n__, __, sprintf } from '../../../locale'; export const getMonthNames = (abbreviated) => { if (abbreviated) { return [ - s__('Jan'), - s__('Feb'), - s__('Mar'), - s__('Apr'), - s__('May'), - s__('Jun'), - s__('Jul'), - s__('Aug'), - s__('Sep'), - s__('Oct'), - s__('Nov'), - s__('Dec'), + __('Jan'), + __('Feb'), + __('Mar'), + __('Apr'), + __('May'), + __('Jun'), + __('Jul'), + __('Aug'), + __('Sep'), + __('Oct'), + __('Nov'), + __('Dec'), ]; } return [ - s__('January'), - s__('February'), - s__('March'), - s__('April'), - s__('May'), - s__('June'), - s__('July'), - s__('August'), - s__('September'), - s__('October'), - s__('November'), - s__('December'), + __('January'), + __('February'), + __('March'), + __('April'), + __('May'), + __('June'), + __('July'), + __('August'), + __('September'), + __('October'), + __('November'), + __('December'), ]; }; diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index 2a60825a427..c9e7b034950 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -130,7 +130,7 @@ export default { }} <a :href="clusterApplicationsDocumentationPath"> <strong> - {{ s__('View Documentation') }} + {{ __('View Documentation') }} </strong> </a> </gl-alert> diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue index 1765a2f3d5d..a63008aa382 100644 --- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue +++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue @@ -63,7 +63,7 @@ export default { return !(this.form.fileName && !this.form.fileName.endsWith('.yml')); }, fileNameFeedback() { - return !this.fileNameState ? s__('The file name should have a .yml extension') : ''; + return !this.fileNameState ? __('The file name should have a .yml extension') : ''; }, }, mounted() { diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 831e6dd8f92..33819c78c0f 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -78,8 +78,8 @@ export default { v-if="resolveAllDiscussionsIssuePath && !allResolved" v-gl-tooltip :href="resolveAllDiscussionsIssuePath" - :title="s__('Create issue to resolve all threads')" - :aria-label="s__('Create issue to resolve all threads')" + :title="__('Create issue to resolve all threads')" + :aria-label="__('Create issue to resolve all threads')" class="new-issue-for-discussion discussion-create-issue-btn" icon="issue-new" /> diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index 055d6f40c14..855e06e82ab 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -70,7 +70,7 @@ export default { }, primaryProps() { return { - text: s__('Delete project'), + text: __('Delete project'), attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }], }; }, diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue index 9aac364d20e..c3ac074cd7a 100644 --- a/app/assets/javascripts/pages/groups/new/components/app.vue +++ b/app/assets/javascripts/pages/groups/new/components/app.vue @@ -47,7 +47,7 @@ export default { <template> <new-namespace-page :jump-to-last-persisted-panel="hasErrors" - :initial-breadcrumb="s__('New group')" + :initial-breadcrumb="__('New group')" :panels="$options.PANELS" :title="s__('GroupsNew|Create new group')" persistence-key="new_group_last_active_tab" diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index e42e89ce021..b41611001ab 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -3,7 +3,7 @@ import { GlModal } from '@gitlab/ui'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; export default { components: { @@ -83,7 +83,7 @@ export default { attributes: [{ variant: 'warning' }], }, cancelAction: { - text: s__('Cancel'), + text: __('Cancel'), attributes: [], }, }; diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 0e646e8c505..85443843684 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -72,18 +72,18 @@ export default { return [ { value: KEY_EVERY_DAY, - text: sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }), + text: sprintf(__(`Every day (at %{time})`), { time: this.formattedTime }), }, { value: KEY_EVERY_WEEK, - text: sprintf(s__('Every week (%{weekday} at %{time})'), { + text: sprintf(__('Every week (%{weekday} at %{time})'), { weekday: this.weekday, time: this.formattedTime, }), }, { value: KEY_EVERY_MONTH, - text: sprintf(s__('Every month (Day %{day} at %{time})'), { + text: sprintf(__('Every month (Day %{day} at %{time})'), { day: this.randomDay, time: this.formattedTime, }), diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 8deb955842c..384ee1f5034 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -2,7 +2,7 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui'; import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import { visibilityOptions, visibilityLevelDescriptions, @@ -31,7 +31,7 @@ export default { operationsLabel: s__('ProjectSettings|Operations'), packagesLabel: s__('ProjectSettings|Packages'), pagesLabel: s__('ProjectSettings|Pages'), - ciCdLabel: s__('CI/CD'), + ciCdLabel: __('CI/CD'), repositoryLabel: s__('ProjectSettings|Repository'), requirementsLabel: s__('ProjectSettings|Requirements'), securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'), diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index a8ec731e105..2ce1f0366c1 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -12,7 +12,7 @@ import { import axios from '~/lib/utils/axios_utils'; import csrf from '~/lib/utils/csrf'; import { setUrlFragment } from '~/lib/utils/url_utility'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import Tracking from '~/tracking'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { @@ -83,7 +83,7 @@ export default { ), }, }, - feedbackTip: s__( + feedbackTip: __( 'Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}.', ), }, diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index f163a7c3a8e..1bb82e1d8e6 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import { sortOrders, sortOrderOptions } from '../constants'; import RequestWarning from './request_warning.vue'; @@ -55,7 +55,7 @@ export default { const summary = {}; if (!this.metricDetails.summaryOptions?.hideTotal) { - summary[s__('Total')] = this.metricDetails.calls; + summary[__('Total')] = this.metricDetails.calls; } if (!this.metricDetails.summaryOptions?.hideDuration) { diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue index c1739f51f6b..f27c2cd9dca 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue @@ -12,7 +12,7 @@ import { produce } from 'immer'; import { fetchPolicies } from '~/lib/graphql'; import { historyPushState } from '~/lib/utils/common_utils'; import { setUrlParams } from '~/lib/utils/url_utility'; -import { s__ } from '~/locale'; +import { __ } from '~/locale'; import { BRANCH_PAGINATION_LIMIT, BRANCH_SEARCH_DEBOUNCE, @@ -25,9 +25,9 @@ import getLastCommitBranchQuery from '~/pipeline_editor/graphql/queries/client/l export default { i18n: { - dropdownHeader: s__('Switch branch'), - title: s__('Branches'), - fetchError: s__('Unable to fetch branch list for this project.'), + dropdownHeader: __('Switch branch'), + title: __('Branches'), + fetchError: __('Unable to fetch branch list for this project.'), }, inputDebounce: BRANCH_SEARCH_DEBOUNCE, components: { diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 17cbcabeedb..3cb2dce87d3 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -37,7 +37,7 @@ export default { }, primaryProps() { return { - text: s__('Delete account'), + text: __('Delete account'), attributes: [ { variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' }, { category: 'primary' }, @@ -47,7 +47,7 @@ export default { }, cancelProps() { return { - text: s__('Cancel'), + text: __('Cancel'), }; }, canSubmit() { diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index 7917a9a75e0..45a6130826d 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -3,7 +3,7 @@ import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } import { escape } from 'lodash'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; export default { components: { @@ -58,7 +58,7 @@ Please update your Git repository remotes as soon as possible.`), }, primaryProps() { return { - text: s__('Update username'), + text: __('Update username'), attributes: [ { variant: 'warning' }, { category: 'primary' }, @@ -68,7 +68,7 @@ Please update your Git repository remotes as soon as possible.`), }, cancelProps() { return { - text: s__('Cancel'), + text: __('Cancel'), }; }, }, diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue index 6e9efc50be8..476d6466cbb 100644 --- a/app/assets/javascripts/projects/new/components/app.vue +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -95,7 +95,7 @@ export default { <template> <new-namespace-page - :initial-breadcrumb="s__('New project')" + :initial-breadcrumb="__('New project')" :panels="availablePanels" :jump-to-last-persisted-panel="hasErrors" :title="s__('ProjectsNew|Create new project')" diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js index 12d56256b9a..df4b1800dff 100644 --- a/app/assets/javascripts/projects/storage_counter/constants.js +++ b/app/assets/javascripts/projects/storage_counter/constants.js @@ -51,7 +51,7 @@ export const ERROR_MESSAGE = s__( 'UsageQuota|Something went wrong while fetching project storage statistics', ); -export const LEARN_MORE_LABEL = s__('Learn more.'); +export const LEARN_MORE_LABEL = __('Learn more.'); export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas'); export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link'); export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A'); diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index f6f409873c8..a79da00de43 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -58,7 +58,7 @@ export default { }; this.isLoading = false; createFlash({ - message: s__('Something went wrong on our end'), + message: __('Something went wrong on our end'), }); }, initPolling() { diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue index d6187e1b527..50835142d28 100644 --- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -1,7 +1,7 @@ <script> import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; -import { sprintf, n__, s__ } from '~/locale'; +import { sprintf, __, n__ } from '~/locale'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; import { parseIssuableData } from '../../issue_show/utils/parse_data'; @@ -40,7 +40,7 @@ export default { this.totalCount, ); - return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText }); + return sprintf(__('%{mrText}, this issue will be closed automatically.'), { mrText }); }, }, mounted() { diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js index 652d03a0fd0..94abb50de89 100644 --- a/app/assets/javascripts/related_merge_requests/store/actions.js +++ b/app/assets/javascripts/related_merge_requests/store/actions.js @@ -1,7 +1,7 @@ import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils'; -import { s__ } from '~/locale'; +import { __ } from '~/locale'; import * as types from './mutation_types'; const REQUEST_PAGE_COUNT = 100; @@ -30,7 +30,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => { .catch(() => { dispatch('receiveDataError'); createFlash({ - message: s__('Something went wrong while fetching related merge requests.'), + message: __('Something went wrong while fetching related merge requests.'), }); }); }; diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue index 73911b9d319..aa7c26b8044 100644 --- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue @@ -1,7 +1,7 @@ <script> import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; -import { sprintf, s__ } from '~/locale'; +import { sprintf, __ } from '~/locale'; export default { name: 'RadioFilter', @@ -49,7 +49,7 @@ export default { ...mapActions(['setQuery']), radioLabel(filter) { return filter.value === this.ANY.value - ? sprintf(s__('Any %{header}'), { header: this.filterData.header.toLowerCase() }) + ? sprintf(__('Any %{header}'), { header: this.filterData.header.toLowerCase() }) : filter.label; }, }, diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue index 70e692a0c86..eb03aa3cca3 100644 --- a/app/assets/javascripts/static_site_editor/pages/success.vue +++ b/app/assets/javascripts/static_site_editor/pages/success.vue @@ -30,7 +30,7 @@ export default { updatedFileDescription() { const { sourcePath } = this.appData; - return sprintf(s__('Update %{sourcePath} file'), { sourcePath }); + return sprintf(__('Update %{sourcePath} file'), { sourcePath }); }, }, created() { diff --git a/app/assets/javascripts/user_lists/components/user_lists_table.vue b/app/assets/javascripts/user_lists/components/user_lists_table.vue index 765f59228a6..ccc2bfabb56 100644 --- a/app/assets/javascripts/user_lists/components/user_lists_table.vue +++ b/app/assets/javascripts/user_lists/components/user_lists_table.vue @@ -23,7 +23,7 @@ export default { translations: { createdTimeagoLabel: s__('UserList|created %{timeago}'), deleteListTitle: s__('UserList|Delete %{name}?'), - deleteListMessage: s__('User list %{name} will be removed. Are you sure?'), + deleteListMessage: __('User list %{name} will be removed. Are you sure?'), editUserListLabel: s__('FeatureFlags|Edit User List'), }, modal: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue index 533af6a5e51..fc17669a737 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue @@ -67,8 +67,8 @@ export default { <template #targetBranch> <span class="label-branch">{{ targetBranchEscaped }}</span> </template> - <template v-if="glFeatures.restructuredMrWidget" #squashedCommits> - <template v-if="isSquashEnabled"> + <template #squashedCommits> + <template v-if="glFeatures.restructuredMrWidget && isSquashEnabled"> {{ __('(commits will be squashed)') }}</template ></template > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 5c67b9c7ab5..9070cb1fe65 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -151,7 +151,7 @@ export default { right data-qa-selector="download_dropdown" > - <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header> + <gl-dropdown-section-header>{{ __('Download as') }}</gl-dropdown-section-header> <gl-dropdown-item :href="mr.emailPatchesPath" class="js-download-email-patches" diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js index 779b04dc2bd..fc0ff78e7b4 100644 --- a/app/assets/javascripts/vue_shared/directives/validation.js +++ b/app/assets/javascripts/vue_shared/directives/validation.js @@ -1,4 +1,4 @@ -import { s__ } from '~/locale'; +import { __ } from '~/locale'; /** * Validation messages will take priority based on the property order. @@ -12,11 +12,11 @@ import { s__ } from '~/locale'; const defaultFeedbackMap = { valueMissing: { isInvalid: (el) => el.validity?.valueMissing, - message: s__('Please fill out this field.'), + message: __('Please fill out this field.'), }, urlTypeMismatch: { isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch, - message: s__('Please enter a valid URL format, ex: http://www.example.com/home'), + message: __('Please enter a valid URL format, ex: http://www.example.com/home'), }, }; diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb index bbeb74faf2b..1cd80174f93 100644 --- a/app/helpers/ci/runners_helper.rb +++ b/app/helpers/ci/runners_helper.rb @@ -6,27 +6,30 @@ module Ci def runner_status_icon(runner, size: 16, icon_class: '') status = runner.status + active = runner.active title = '' icon = 'warning-solid' span_class = '' case status + when :online + if active + title = s_("Runners|Runner is online, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) } + icon = 'status-active' + span_class = 'gl-text-green-500' + else + title = s_("Runners|Runner is paused, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) } + icon = 'status-paused' + span_class = 'gl-text-gray-600' + end when :not_connected title = s_("Runners|New runner, has not connected yet") icon = 'warning-solid' - when :online - title = s_("Runners|Runner is online, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) } - icon = 'status-active' - span_class = 'gl-text-green-500' when :offline title = s_("Runners|Runner is offline, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) } icon = 'status-failed' span_class = 'gl-text-red-500' - when :paused - title = s_("Runners|Runner is paused, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) } - icon = 'status-paused' - span_class = 'gl-text-gray-600' end content_tag(:span, class: span_class, title: title, data: { toggle: 'tooltip', container: 'body', testid: 'runner_status_icon', qa_selector: "runner_status_#{status}_content" }) do diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 13f1302bdb0..8a3025e5608 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -274,6 +274,14 @@ module Ci end def status + return :not_connected unless contacted_at + + online? ? :online : :offline + end + + # DEPRECATED + # TODO Remove in %15.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648 + def deprecated_rest_status if contacted_at.nil? :not_connected elsif active? diff --git a/config/feature_flags/development/use_model_load_balancing.yml b/config/feature_flags/development/use_model_load_balancing.yml new file mode 100644 index 00000000000..630e34acff3 --- /dev/null +++ b/config/feature_flags/development/use_model_load_balancing.yml @@ -0,0 +1,8 @@ +--- +name: use_model_load_balancing +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73631 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344797 +milestone: '14.5' +type: development +group: group::sharding +default_enabled: false diff --git a/data/deprecations/14-5-runner-api-status-does-contain-paused.yml b/data/deprecations/14-5-runner-api-status-does-contain-paused.yml new file mode 100644 index 00000000000..4ce817313ce --- /dev/null +++ b/data/deprecations/14-5-runner-api-status-does-contain-paused.yml @@ -0,0 +1,16 @@ +- name: "REST API Runner will not contain 'paused'" + announcement_milestone: "14.5" # The milestone when this feature was first announced as deprecated. + removal_milestone: "15.0" # the milestone when this feature is planned to be removed + body: | # Do not modify this line, instead modify the lines below. + Runner REST API will not return "paused" as a status in GitLab 15.0. + + REST API: Paused runners' status will only relate to runner contact status, such as: + "online", "offline", "not_connected". Status "paused" will not appear when the runner is + not active. + + When checking if a runner is "paused", API users are advised to check the boolean attribute + "active" to be `false` instead. + stage: Verify + tiers: [Core, Premium, Ultimate] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344648 + documentation_url: https://docs.gitlab.com/ee/api/runners.html diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 6d2f65f6953..da456131a52 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -1585,11 +1585,29 @@ all state associated with a given repository including: sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml remove-repository -virtual-storage <virtual-storage> -repository <repository> ``` -- `-virtual-storage` is the virtual storage the repository is located in. -- `-repository` is the repository's relative path in the storage. +- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following: -Sometimes parts of the repository continue to exist after running `remove-repository`. This can be caused -because of: + ```ruby + praefect['virtual_storages'] = { + 'default' => { + ... + }, + 'storage-1' => { + ... + } + } + ``` + + In this example, the virtual storage to specify is `default` or `storage-1`. + +- `-repository` is the repository's relative path in the storage [beginning with `@hashed`](../repository_storage_types.md#hashed-storage). + For example: + + ```plaintext + @hashed/f5/ca/f5ca38f748a1d6eaf726b8a42fb575c3c71f1864a8143301782de13da2d9202b.git + ``` + +Parts of the repository can continue to exist after running `remove-repository`. This can be because of: - A deletion error. - An in-flight RPC call targeting the repository. @@ -1613,8 +1631,53 @@ The command outputs: Each entry is a complete JSON string with a newline at the end (configurable using the `-delimiter` flag). For example: -```shell +```plaintext sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml list-untracked-repositories {"virtual_storage":"default","storage":"gitaly-1","relative_path":"@hashed/ab/cd/abcd123456789012345678901234567890123456789012345678901234567890.git"} {"virtual_storage":"default","storage":"gitaly-1","relative_path":"@hashed/ab/cd/abcd123456789012345678901234567890123456789012345678901234567891.git"} ``` + +### Manually track repositories + +> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5658) in GitLab 14.4. + +The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked. + +```shell +sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -repository <repository> +``` + +- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following: + + ```ruby + praefect['virtual_storages'] = { + 'default' => { + ... + }, + 'storage-1' => { + ... + } + } + ``` + + In this example, the virtual storage to specify is `default` or `storage-1`. + +- `-repository` is the repository's relative path in the storage [beginning with `@hashed`](../repository_storage_types.md#hashed-storage). + For example: + + ```plaintext + @hashed/f5/ca/f5ca38f748a1d6eaf726b8a42fb575c3c71f1864a8143301782de13da2d9202b.git + ``` + +- `-authoritative-storage` is the storage we want Praefect to treat as the primary. Required if + [per-repository replication](#configure-replication-factor) is set as the replication strategy. + +The command outputs: + +- Results to `STDOUT` and the command's logs. +- Errors to `STDERR`. + +This command fails if: + +- The repository is already being tracked by the Praefect database. +- The repository does not exist on disk. diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 91643cd3ee0..82fd62d8d79 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -83,6 +83,24 @@ request is as follows: migrations on a fresh database before the MR is reviewed. If the review leads to large changes in the MR, execute the migrations again once the review is complete. 1. Write tests for more complex migrations. +1. If your merge request adds new validations to existing models, to make sure the + data processing is backwards compatible: + + - Ask in the [`#database`](https://gitlab.slack.com/archives/CNZ8E900G) Slack channel + for assistance to execute the database query that checks the existing rows to + ensure existing rows aren't impacted by the change. + - Add the necessary validation with a feature flag to be gradually rolled out + following [the rollout steps](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#rollout). + + If this merge request is urgent, the code owners should make the final call on + whether reviewing existing rows should be included as an immediate follow-up task + to the merge request. + + NOTE: + There isn't a way to know anything about our customers' data on their + [self-managed instances](../../subscriptions/self_managed/index.md), so keep + that in mind for any data implications with your merge request. + 1. Merge requests **must** adhere to the [merge request performance guidelines](../merge_request_performance_guidelines.md). 1. For tests that use Capybara, read [how to write reliable, asynchronous integration tests](https://thoughtbot.com/blog/write-reliable-asynchronous-integration-tests-with-capybara). diff --git a/lib/api/entities/ci/runner.rb b/lib/api/entities/ci/runner.rb index ede698696de..60193fe1df4 100644 --- a/lib/api/entities/ci/runner.rb +++ b/lib/api/entities/ci/runner.rb @@ -12,7 +12,9 @@ module API expose :runner_type expose :name expose :online?, as: :online - expose :status + # DEPRECATED + # TODO Remove in %15.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648 + expose :status, as: :deprecated_rest_status end end end diff --git a/lib/gitlab/database/load_balancing/setup.rb b/lib/gitlab/database/load_balancing/setup.rb index a44ace9da26..bcb40e45dac 100644 --- a/lib/gitlab/database/load_balancing/setup.rb +++ b/lib/gitlab/database/load_balancing/setup.rb @@ -5,7 +5,7 @@ module Gitlab module LoadBalancing # Class for setting up load balancing of a specific model. class Setup - attr_reader :configuration + attr_reader :model, :configuration def initialize(model, start_service_discovery: false) @model = model @@ -15,8 +15,9 @@ module Gitlab def setup configure_connection - setup_load_balancer + setup_connection_proxy setup_service_discovery + setup_feature_flag_to_model_load_balancing end def configure_connection @@ -36,28 +37,82 @@ module Gitlab @model.establish_connection(hash_config) end - def setup_load_balancer - lb = LoadBalancer.new(configuration) - + def setup_connection_proxy # We just use a simple `class_attribute` here so we don't need to # inject any modules and/or expose unnecessary methods. - @model.class_attribute(:connection) - @model.class_attribute(:sticking) + setup_class_attribute(:connection, ConnectionProxy.new(load_balancer)) + setup_class_attribute(:sticking, Sticking.new(load_balancer)) + end + + # TODO: This is temporary code to gradually redirect traffic to use + # a dedicated DB replicas, or DB primaries (depending on configuration) + # This implements a sticky behavior for the current request if enabled. + # + # This is needed for Phase 3 and Phase 4 of application rollout + # https://gitlab.com/groups/gitlab-org/-/epics/6160#progress + # + # If `GITLAB_USE_MODEL_LOAD_BALANCING` is set, its value is preferred + # Otherwise, a `use_model_load_balancing` FF value is used + def setup_feature_flag_to_model_load_balancing + return if active_record_base? - @model.connection = ConnectionProxy.new(lb) - @model.sticking = Sticking.new(lb) + @model.singleton_class.prepend(ModelLoadBalancingFeatureFlagMixin) end def setup_service_discovery return unless configuration.service_discovery_enabled? - lb = @model.connection.load_balancer - sv = ServiceDiscovery.new(lb, **configuration.service_discovery) + sv = ServiceDiscovery.new(load_balancer, **configuration.service_discovery) sv.perform_service_discovery sv.start if @start_service_discovery end + + def load_balancer + @load_balancer ||= LoadBalancer.new(configuration) + end + + private + + def setup_class_attribute(attribute, value) + @model.class_attribute(attribute) + @model.public_send("#{attribute}=", value) # rubocop:disable GitlabSecurity/PublicSend + end + + def active_record_base? + @model == ActiveRecord::Base + end + + module ModelLoadBalancingFeatureFlagMixin + extend ActiveSupport::Concern + + def use_model_load_balancing? + # Cache environment variable and return env variable first if defined + use_model_load_balancing_env = Gitlab::Utils.to_boolean(ENV["GITLAB_USE_MODEL_LOAD_BALANCING"]) + + unless use_model_load_balancing_env.nil? + return use_model_load_balancing_env + end + + # Check a feature flag using RequestStore (if active) + return false unless Gitlab::SafeRequestStore.active? + + Gitlab::SafeRequestStore.fetch(:use_model_load_balancing) do + Feature.enabled?(:use_model_load_balancing, default_enabled: :yaml) + end + end + + # rubocop:disable Database/MultipleDatabases + def connection + use_model_load_balancing? ? super : ActiveRecord::Base.connection + end + + def sticking + use_model_load_balancing? ? super : ActiveRecord::Base.sticking + end + # rubocop:enable Database/MultipleDatabases + end end end end diff --git a/lib/gitlab/import_export/attributes_permitter.rb b/lib/gitlab/import_export/attributes_permitter.rb index 2d8e25a9f70..f6f65f85599 100644 --- a/lib/gitlab/import_export/attributes_permitter.rb +++ b/lib/gitlab/import_export/attributes_permitter.rb @@ -44,7 +44,7 @@ module Gitlab # We want to use AttributesCleaner for these relations instead, in the future this should be removed to make sure # we are using AttributesPermitter for every imported relation. - DISABLED_RELATION_NAMES = %i[user author issuable_sla].freeze + DISABLED_RELATION_NAMES = %i[author issuable_sla].freeze def initialize(config: ImportExport::Config.new.to_h) @config = config diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index bbcf802daf6..52cbf6ce750 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -178,17 +178,7 @@ included_attributes: - :project_id - :key - :value - label: - - :title - - :color - - :project_id - - :group_id - - :created_at - - :updated_at - - :template - - :description - - :priority - labels: + label: &label_definition - :title - :color - :project_id @@ -198,23 +188,13 @@ included_attributes: - :template - :description - :priority + labels: *label_definition priorities: - :project_id - :priority - :created_at - :updated_at - milestone: - - :iid - - :title - - :project_id - - :group_id - - :description - - :due_date - - :created_at - - :updated_at - - :start_date - - :state - milestones: + milestone: &milestone_definition - :iid - :title - :project_id @@ -225,6 +205,7 @@ included_attributes: - :updated_at - :start_date - :state + milestones: *milestone_definition protected_branches: - :project_id - :name @@ -315,6 +296,200 @@ included_attributes: - :project_id - :issue_template_key - :project_key + snippets: + - :title + - :content + - :author_id + - :project_id + - :created_at + - :updated_at + - :file_name + - :visibility_level + - :description + project_members: + - :access_level + - :source_type + - :user_id + - :notification_level + - :created_at + - :updated_at + - :created_by_id + - :invite_email + - :invite_accepted_at + - :requested_at + - :expires_at + - :ldap + - :override + merge_request: &merge_request_definition + - :target_branch + - :source_branch + - :source_project_id + - :author_id + - :assignee_id + - :title + - :created_at + - :updated_at + - :state + - :merge_status + - :target_project_id + - :iid + - :description + - :updated_by_id + - :merge_error + - :merge_params + - :merge_when_pipeline_succeeds + - :merge_user_id + - :merge_commit_sha + - :squash_commit_sha + - :in_progress_merge_commit_sha + - :lock_version + - :approvals_before_merge + - :rebase_commit_sha + - :time_estimate + - :squash + - :last_edited_at + - :last_edited_by_id + - :discussion_locked + - :allow_maintainer_to_push + - :merge_ref_sha + - :draft + - :diff_head_sha + - :source_branch_sha + - :target_branch_sha + merge_requests: *merge_request_definition + award_emoji: + - :user_id + - :name + - :awardable_type + - :created_at + - :updated_at + commit_author: + - :name + - :email + committer: + - :name + - :email + events: + - :target_type + - :action + - :author_id + - :fingerprint + - :created_at + - :updated_at + label_links: + - :target_type + - :created_at + - :updated_at + merge_request_diff: + - :state + - :created_at + - :updated_at + - :base_commit_sha + - :real_size + - :head_commit_sha + - :start_commit_sha + - :commits_count + - :files_count + - :sorted + - :diff_type + merge_request_diff_commits: + - :relative_order + - :sha + - :authored_date + - :committed_date + - :message + - :trailers + merge_request_diff_files: + - :relative_order + - :new_file + - :renamed_file + - :deleted_file + - :new_path + - :old_path + - :a_mode + - :b_mode + - :too_large + - :binary + - :diff + metrics: + - :created_at + - :updated_at + - :latest_closed_by_id + - :latest_closed_at + - :merged_by_id + - :merged_at + - :latest_build_started_at + - :latest_build_finished_at + - :first_deployed_to_production_at + - :first_comment_at + - :first_commit_at + - :last_commit_at + - :diff_size + - :modified_paths_size + - :commits_count + - :first_approved_at + - :first_reassigned_at + - :added_lines + - :target_project_id + - :removed_lines + notes: + - :note + - :noteable_type + - :author_id + - :created_at + - :updated_at + - :project_id + - :attachment + - :line_code + - :commit_id + - :system + - :st_diff + - :updated_by_id + - :type + - :position + - :original_position + - :change_position + - :resolved_at + - :resolved_by_id + - :resolved_by_push + - :discussion_id + - :confidential + - :last_edited_at + push_event_payload: + - :commit_count + - :action + - :ref_type + - :commit_from + - :commit_to + - :ref + - :commit_title + - :ref_count + resource_label_events: + - :action + - :user_id + - :created_at + suggestions: + - :relative_order + - :applied + - :commit_id + - :from_content + - :to_content + - :outdated + - :lines_above + - :lines_below + system_note_metadata: + - :commit_count + - :action + - :created_at + - :updated_at + timelogs: + - :time_spent + - :user_id + - :project_id + - :spent_at + - :created_at + - :updated_at + - :summary # Do not include the following attributes for the models specified. excluded_attributes: @@ -430,16 +605,7 @@ excluded_attributes: - :service_desk_reply_to - :upvotes_count - :work_item_type_id - merge_request: - - :milestone_id - - :sprint_id - - :ref_fetched - - :merge_jid - - :rebase_jid - - :latest_merge_request_diff_id - - :head_pipeline_id - - :state_id - merge_requests: + merge_request: &merge_request_excluded_definition - :milestone_id - :sprint_id - :ref_fetched @@ -448,6 +614,7 @@ excluded_attributes: - :latest_merge_request_diff_id - :head_pipeline_id - :state_id + merge_requests: *merge_request_excluded_definition award_emoji: - :awardable_id statuses: @@ -516,10 +683,9 @@ excluded_attributes: - :issue_id zoom_meetings: - :issue_id - design: - - :issue_id - designs: + design: &design_excluded_definition - :issue_id + designs: *design_excluded_definition design_versions: - :issue_id actions: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b450013e3bc..f16a436ecf6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -38128,9 +38128,6 @@ msgstr "" msgid "VulnerabilityChart|Severity" msgstr "" -msgid "VulnerabilityManagement, Fetching linked Jira issues" -msgstr "" - msgid "VulnerabilityManagement|%{statusStart}Confirmed%{statusEnd} %{timeago} by %{user}" msgstr "" @@ -38158,6 +38155,9 @@ msgstr "" msgid "VulnerabilityManagement|Detected" msgstr "" +msgid "VulnerabilityManagement|Fetching linked Jira issues" +msgstr "" + msgid "VulnerabilityManagement|Needs triage" msgstr "" diff --git a/package.json b/package.json index 791e475341b..9bf46e477c2 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@gitlab/eslint-plugin": "9.4.0", + "@gitlab/eslint-plugin": "10.0.0", "@gitlab/stylelint-config": "2.6.0", "@testing-library/dom": "^7.16.2", "@vue/test-utils": "1.2.0", diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb index 971e7634f6d..cdfd7fbdad9 100644 --- a/qa/qa/page/component/issuable/sidebar.rb +++ b/qa/qa/page/component/issuable/sidebar.rb @@ -22,20 +22,16 @@ module QA element :labels_block end - base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue' do - element :selected_label_content + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do + element :dropdown_input_field end - base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue' do element :labels_dropdown_content end - base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do - element :labels_edit_button - end - - base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do - element :dropdown_input_field + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue' do + element :selected_label_content end base.view 'app/views/shared/issuable/_sidebar.html.haml' do @@ -53,7 +49,7 @@ module QA end def assign_milestone(milestone) - within_element(:milestone_block) do + wait_milestone_block_finish_loading do click_element(:edit_link) click_on(milestone.title) end @@ -70,14 +66,14 @@ module QA end def has_assignee?(username) - within_element(:assignee_block) do - has_text?(username, wait: 1) + wait_assignees_block_finish_loading do + has_text?(username) end end def has_no_assignee?(username) - within_element(:assignee_block) do - has_no_text?(username, wait: 1) + wait_assignees_block_finish_loading do + has_no_text?(username) end end @@ -88,8 +84,14 @@ module QA end def has_label?(label) - within_element(:labels_block) do - !!has_element?(:selected_label_content, label_name: label) + wait_labels_block_finish_loading do + has_element?(:selected_label_content, label_name: label) + end + end + + def has_no_label?(label) + wait_labels_block_finish_loading do + has_no_element?(:selected_label_content, label_name: label) end end @@ -103,27 +105,19 @@ module QA find_element(:more_assignees_link) end - def select_labels_and_refresh(labels) - Support::Retrier.retry_until do - click_element(:labels_edit_button) - has_element?(:labels_dropdown_content, text: labels.first) - end + def select_labels(labels) + within_element(:labels_block) do + click_element(:edit_link) - labels.each do |label| - within_element(:labels_dropdown_content) do - send_keys_to_element(:dropdown_input_field, [label, :enter]) + labels.each do |label| + within_element(:labels_dropdown_content) do + fill_element(:dropdown_input_field, label) + click_button(text: label) + end end end - click_element(:labels_edit_button) - - labels.each do |label| - has_element?(:labels_block, text: label, wait: 0) - end - - refresh - - wait_for_requests + click_element(:title) # to blur dropdown end def toggle_more_assignees_link @@ -141,6 +135,15 @@ module QA end end + def wait_labels_block_finish_loading + within_element(:labels_block) do + wait_until(reload: false, max_duration: 10, sleep_interval: 1) do + finished_loading_block? + yield + end + end + end + def wait_milestone_block_finish_loading within_element(:milestone_block) do wait_until(reload: false, max_duration: 10, sleep_interval: 1) do diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb index 7001f53a7b7..47d2d246460 100644 --- a/qa/qa/support/matchers/have_matcher.rb +++ b/qa/qa/support/matchers/have_matcher.rb @@ -19,6 +19,7 @@ module QA related_issue_item snippet_description tag + label ].each do |predicate| RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs| match do |page_object| diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 691cdb0f676..b63236dec82 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -1,15 +1,15 @@ -import { GlAlert, GlButton, GlLink } from '@gitlab/ui'; +import { GlAlert, GlLink } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; +import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { __ } from '~/locale'; jest.mock('~/jira_connect/subscriptions/utils', () => ({ retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }), - getLocation: jest.fn(), })); describe('JiraConnectApp', () => { @@ -18,7 +18,7 @@ describe('JiraConnectApp', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findAlertLink = () => findAlert().findComponent(GlLink); - const findGlButton = () => wrapper.findComponent(GlButton); + const findSignInButton = () => wrapper.findComponent(SignInButton); const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); const createComponent = ({ provide, mountFn = shallowMount } = {}) => { @@ -35,28 +35,25 @@ describe('JiraConnectApp', () => { }); describe('template', () => { - describe('when user is not logged in', () => { + describe.each` + scenario | usersPath | expectSignInButton | expectNamespaceButton + ${'user is not signed in'} | ${'/users'} | ${true} | ${false} + ${'user is signed in'} | ${undefined} | ${false} | ${true} + `('when $scenario', ({ usersPath, expectSignInButton, expectNamespaceButton }) => { beforeEach(() => { createComponent({ provide: { - usersPath: '/users', + usersPath, }, }); }); - it('renders "Sign in" button', () => { - expect(findGlButton().text()).toBe('Sign in to add namespaces'); - expect(findAddNamespaceButton().exists()).toBe(false); - }); - }); - - describe('when user is logged in', () => { - beforeEach(() => { - createComponent(); + it('renders sign in button as expected', () => { + expect(findSignInButton().exists()).toBe(expectSignInButton); }); - it('renders "Add namespace" button ', () => { - expect(findAddNamespaceButton().exists()).toBe(true); + it('renders "Add Namespace" button as expected', () => { + expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js new file mode 100644 index 00000000000..8ad5206ce0f --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js @@ -0,0 +1,50 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { getLocation } from '~/jira_connect/subscriptions/utils'; +import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +const MOCK_USERS_PATH = '/user'; + +jest.mock('~/jira_connect/subscriptions/utils'); + +describe('SignInButton', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(SignInButton, { + propsData: { + usersPath: MOCK_USERS_PATH, + }, + }); + }; + + const findButton = () => wrapper.findComponent(GlButton); + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays a button', () => { + createComponent(); + + expect(findButton().exists()).toBe(true); + }); + + describe.each` + getLocationValue | expectedHref + ${''} | ${MOCK_USERS_PATH} + ${undefined} | ${MOCK_USERS_PATH} + ${'https://test.jira.com'} | ${`${MOCK_USERS_PATH}?return_to=${encodeURIComponent('https://test.jira.com')}`} + `('when getLocation resolves with `$getLocationValue`', ({ getLocationValue, expectedHref }) => { + it(`sets button href to ${expectedHref}`, async () => { + getLocation.mockResolvedValue(getLocationValue); + createComponent(); + + expect(getLocation).toHaveBeenCalled(); + await waitForPromises(); + + expect(findButton().attributes('href')).toBe(expectedHref); + }); + }); +}); diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index f6ad41d5478..7a64b654baa 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -185,15 +185,15 @@ describe('dateInWords', () => { const date = new Date('07/01/2016'); it('should return date in words', () => { - expect(datetimeUtility.dateInWords(date)).toEqual(s__('July 1, 2016')); + expect(datetimeUtility.dateInWords(date)).toEqual(__('July 1, 2016')); }); it('should return abbreviated month name', () => { - expect(datetimeUtility.dateInWords(date, true)).toEqual(s__('Jul 1, 2016')); + expect(datetimeUtility.dateInWords(date, true)).toEqual(__('Jul 1, 2016')); }); it('should return date in words without year', () => { - expect(datetimeUtility.dateInWords(date, true, true)).toEqual(s__('Jul 1')); + expect(datetimeUtility.dateInWords(date, true, true)).toEqual(__('Jul 1')); }); }); @@ -201,11 +201,11 @@ describe('monthInWords', () => { const date = new Date('2017-01-20'); it('returns month name from provided date', () => { - expect(datetimeUtility.monthInWords(date)).toBe(s__('January')); + expect(datetimeUtility.monthInWords(date)).toBe(__('January')); }); it('returns abbreviated month name from provided date', () => { - expect(datetimeUtility.monthInWords(date, true)).toBe(s__('Jan')); + expect(datetimeUtility.monthInWords(date, true)).toBe(__('Jan')); }); }); diff --git a/spec/lib/gitlab/database/load_balancing/setup_spec.rb b/spec/lib/gitlab/database/load_balancing/setup_spec.rb index 656c861c72d..dfdf7d19e72 100644 --- a/spec/lib/gitlab/database/load_balancing/setup_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/setup_spec.rb @@ -8,8 +8,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do setup = described_class.new(ActiveRecord::Base) expect(setup).to receive(:configure_connection) - expect(setup).to receive(:setup_load_balancer) + expect(setup).to receive(:setup_connection_proxy) expect(setup).to receive(:setup_service_discovery) + expect(setup).to receive(:setup_feature_flag_to_model_load_balancing) setup.setup end @@ -44,7 +45,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do end end - describe '#setup_load_balancer' do + describe '#setup_connection_proxy' do it 'sets up the load balancer' do model = Class.new(ActiveRecord::Base) setup = described_class.new(model) @@ -58,7 +59,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do .with(setup.configuration) .and_return(lb) - setup.setup_load_balancer + setup.setup_connection_proxy expect(model.connection.load_balancer).to eq(lb) expect(model.sticking) @@ -81,7 +82,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do model = ActiveRecord::Base setup = described_class.new(model) sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery) - lb = model.connection.load_balancer allow(setup.configuration) .to receive(:service_discovery_enabled?) @@ -89,7 +89,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do allow(Gitlab::Database::LoadBalancing::ServiceDiscovery) .to receive(:new) - .with(lb, setup.configuration.service_discovery) + .with(setup.load_balancer, setup.configuration.service_discovery) .and_return(sv) expect(sv).to receive(:perform_service_discovery) @@ -102,7 +102,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do model = ActiveRecord::Base setup = described_class.new(model, start_service_discovery: true) sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery) - lb = model.connection.load_balancer allow(setup.configuration) .to receive(:service_discovery_enabled?) @@ -110,7 +109,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do allow(Gitlab::Database::LoadBalancing::ServiceDiscovery) .to receive(:new) - .with(lb, setup.configuration.service_discovery) + .with(setup.load_balancer, setup.configuration.service_discovery) .and_return(sv) expect(sv).to receive(:perform_service_discovery) @@ -120,4 +119,172 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do end end end + + describe '#setup_feature_flag_to_model_load_balancing', :reestablished_active_record_base do + using RSpec::Parameterized::TableSyntax + + where do + { + "with model LB enabled it picks a dedicated CI connection" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true', + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: false, + ff_use_model_load_balancing: nil, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'ci_replica', write: 'ci' } + } + }, + "with model LB enabled and re-use of primary connection it uses CI connection for reads" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true', + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', + request_store_active: false, + ff_use_model_load_balancing: nil, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'ci_replica', write: 'main' } + } + }, + "with model LB disabled it fallbacks to use main" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false', + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: false, + ff_use_model_load_balancing: nil, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'main_replica', write: 'main' } + } + }, + "with model LB disabled, but re-use configured it fallbacks to use main" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false', + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', + request_store_active: false, + ff_use_model_load_balancing: nil, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'main_replica', write: 'main' } + } + }, + "with FF disabled without RequestStore it uses main" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: false, + ff_use_model_load_balancing: false, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'main_replica', write: 'main' } + } + }, + "with FF enabled without RequestStore sticking of FF does not work, so it fallbacks to use main" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: false, + ff_use_model_load_balancing: true, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'main_replica', write: 'main' } + } + }, + "with FF disabled with RequestStore it uses main" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: true, + ff_use_model_load_balancing: false, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'main_replica', write: 'main' } + } + }, + "with FF enabled with RequestStore it sticks FF and uses CI connection" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, + request_store_active: true, + ff_use_model_load_balancing: true, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'ci_replica', write: 'ci' } + } + }, + "with re-use and FF enabled with RequestStore it sticks FF and uses CI connection for reads" => { + env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, + env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', + request_store_active: true, + ff_use_model_load_balancing: true, + expectations: { + main: { read: 'main_replica', write: 'main' }, + ci: { read: 'ci_replica', write: 'main' } + } + } + } + end + + with_them do + let(:ci_class) do + Class.new(ActiveRecord::Base) do + def self.name + 'Ci::ApplicationRecordTemporary' + end + + establish_connection ActiveRecord::DatabaseConfigurations::HashConfig.new( + Rails.env, + 'ci', + ActiveRecord::Base.connection_db_config.configuration_hash + ) + end + end + + let(:models) do + { + main: ActiveRecord::Base, + ci: ci_class + } + end + + around do |example| + if request_store_active + Gitlab::WithRequestStore.with_request_store do + RequestStore.clear! + + example.run + end + else + example.run + end + end + + before do + # Rewrite `class_attribute` to use rspec mocking and prevent modifying the objects + allow_next_instance_of(described_class) do |setup| + allow(setup).to receive(:configure_connection) + + allow(setup).to receive(:setup_class_attribute) do |attribute, value| + allow(setup.model).to receive(attribute) { value } + end + end + + stub_env('GITLAB_USE_MODEL_LOAD_BALANCING', env_GITLAB_USE_MODEL_LOAD_BALANCING) + stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci) + stub_feature_flags(use_model_load_balancing: ff_use_model_load_balancing) + end + + it 'results match expectations' do + result = models.transform_values do |model| + # Make load balancer to force init with a dedicated replicas connections + described_class.new(model).tap do |subject| + subject.configuration.hosts = [subject.configuration.replica_db_config.host] + subject.setup + end + + load_balancer = model.connection.load_balancer + + { + read: load_balancer.read { |connection| connection.pool.db_config.name }, + write: load_balancer.read_write { |connection| connection.pool.db_config.name } + } + end + + expect(result).to eq(expectations) + end + end + end end diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb index 55422ec3ef2..4fea61ec3a8 100644 --- a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb +++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb @@ -80,8 +80,8 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do let(:attributes_permitter) { described_class.new } - where(:relation_name, :permitted_attributes_defined) do - :user | false + where(:relation_name, :permitted_attributes_defined ) do + :user | true :author | false :ci_cd_settings | true :metrics_setting | true @@ -91,6 +91,7 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do :auto_devops | true :boards | true :custom_attributes | true + :label | true :labels | true :protected_branches | true :protected_tags | true @@ -99,6 +100,28 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do :push_access_levels | true :releases | true :links | true + :priorities | true + :milestone | true + :milestones | true + :snippets | true + :project_members | true + :merge_request | true + :merge_requests | true + :award_emoji | true + :commit_author | true + :committer | true + :events | true + :label_links | true + :merge_request_diff | true + :merge_request_diff_commits | true + :merge_request_diff_files | true + :metrics | true + :notes | true + :push_event_payload | true + :resource_label_events | true + :suggestions | true + :system_note_metadata | true + :timelogs | true :container_expiration_policy | true :project_feature | true :prometheus_metrics | true @@ -113,9 +136,11 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do describe 'included_attributes for Project' do subject { described_class.new } + additional_attributes = { user: %w[id] } + Gitlab::ImportExport::Config.new.to_h[:included_attributes].each do |relation_sym, permitted_attributes| context "for #{relation_sym}" do - it_behaves_like 'a permitted attribute', relation_sym, permitted_attributes + it_behaves_like 'a permitted attribute', relation_sym, permitted_attributes, additional_attributes[relation_sym] end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 08c471c6bfb..2e79159cc60 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -626,7 +626,7 @@ RSpec.describe Ci::Runner do end describe '#status' do - let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) } + let(:runner) { build(:ci_runner, :instance) } subject { runner.status } @@ -638,6 +638,45 @@ RSpec.describe Ci::Runner do it { is_expected.to eq(:not_connected) } end + context 'inactive but online' do + before do + runner.contacted_at = 1.second.ago + runner.active = false + end + + it { is_expected.to eq(:online) } + end + + context 'contacted 1s ago' do + before do + runner.contacted_at = 1.second.ago + end + + it { is_expected.to eq(:online) } + end + + context 'contacted long time ago' do + before do + runner.contacted_at = 1.year.ago + end + + it { is_expected.to eq(:offline) } + end + end + + describe '#deprecated_rest_status' do + let(:runner) { build(:ci_runner, :instance, contacted_at: 1.second.ago) } + + subject { runner.deprecated_rest_status } + + context 'never connected' do + before do + runner.contacted_at = nil + end + + it { is_expected.to eq(:not_connected) } + end + context 'contacted 1s ago' do before do runner.contacted_at = 1.second.ago diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index d0257f6c2b6..806c60d5aff 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -162,12 +162,12 @@ RSpec.describe Clusters::Applications::Runner do it 'pauses associated runner' do active_runner = create(:ci_runner, contacted_at: 1.second.ago) - expect(active_runner.status).to eq(:online) + expect(active_runner.active).to be_truthy application_runner = create(:clusters_applications_runner, :scheduled, runner: active_runner) application_runner.prepare_uninstall - expect(active_runner.status).to eq(:paused) + expect(active_runner.active).to be_falsey end end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb index 5ce698c4701..41d3d76b66b 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes| +RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes, additional_attributes = []| let(:prohibited_attributes) { %i[remote_url my_attributes my_ids token my_id test] } let(:import_export_config) { Gitlab::ImportExport::Config.new.to_h } @@ -26,7 +26,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib end it 'does not contain attributes that would be cleaned with AttributeCleaner' do - expect(cleaned_hash.keys).to include(*permitted_hash.keys) + expect(cleaned_hash.keys + additional_attributes.to_a).to include(*permitted_hash.keys) end it 'does not contain prohibited attributes that are not related to given relation' do diff --git a/yarn.lock b/yarn.lock index 2377c548458..54b1473a65b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -873,10 +873,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== -"@gitlab/eslint-plugin@9.4.0": - version "9.4.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-9.4.0.tgz#cad8f63b7985c22865859cc7d2688eb446ad0bbb" - integrity sha512-llPypEQrm9/6Xas5GCoSPAK7W/DgO7CKhzDvAk/Ea9BP0rI2+t8Wg4PFhE1XDctYnnUIS/GrdqVKQkpODk24hQ== +"@gitlab/eslint-plugin@10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-10.0.0.tgz#83430fb4d0a2467bb54975d0b5b9dc8016005722" + integrity sha512-frCYzjQQaZ5kW1on3XwuVGhvYa6XjD6Q1POTbxDpzl6tNxSeTwOJohC6Joyw76e0Kw4fPQd/fHAfKQAB0AVQ7A== dependencies: babel-eslint "^10.0.3" eslint-config-airbnb-base "^14.2.1" |